skip to content

Fitness functions: arquitectura que no se degrada

9 min read

Qué son las fitness functions (pruebas que convierten una regla de arquitectura en un test ejecutable), de dónde vienen y cómo Aurora las usa para que un ERP modular no se erosione con cada módulo nuevo.

Toda arquitectura se degrada sola

Hay una segunda ley de la termodinámica para el software: la arquitectura tiende al desorden. El diagrama bonito que dibujamos al inicio envejece; la regla que lo sostiene vive en la cabeza de alguien o en un documento que nadie vuelve a abrir. Y entonces llega el pull request que, “solo por esta vez”, cruza una frontera. Nadie lo decide. Simplemente pasa.

El síntoma clásico tiene nombre: drift o erosión arquitectónica. El documento dice “el kernel contable no depende de ningún otro módulo”. Seis meses y veinte PRs después, ese kernel importa tres módulos. No hubo una reunión donde se aprobara romper la regla; se rompió por acumulación, una concesión razonable a la vez.

La pregunta interesante no es “¿cómo escribo la regla?”, sino “¿cómo hago que la regla se defienda sola?”. Ahí entran las fitness functions.

Qué es una fitness function

Una fitness function es una prueba automatizada que verifica una propiedad arquitectónica, no lógica de negocio sino una regla estructural que debe sostenerse siempre. Su mecanismo es simple y poderoso: tomas una regla escrita en prosa y la conviertes en un test ejecutable. Si alguien la viola, el build se pone rojo.

El término no es jerga interna de un equipo: viene de los algoritmos evolutivos, donde una fitness function mide qué tan cerca está una solución candidata del objetivo. La metáfora se traslada con elegancia al software: la arquitectura “evoluciona” con cada cambio, y la fitness function mide si ese cambio la acerca o la aleja de las propiedades que quieres preservar.

No es una ocurrencia: el tema tiene literatura propia. El texto de cabecera es Building Evolutionary Architectures (Neal Ford, Rebecca Parsons y Patrick Kua; O’Reilly, 2017; 2.ª edición en 2022). Ahí se acuña el término como concepto formal, y desde ThoughtWorks se empujó hasta el cuadrante Adopt de su Technology Radar.

Invariante en prosa Fitness function arch(…)->expect(…) CI suite Arch ✓ merge ✗ build rojo
De la regla a la barrera: una invariante deja de ser un párrafo y pasa a ser un test que se ejecuta en cada cambio.

No todas son iguales

El libro propone un vocabulario para clasificarlas, y vale la pena conocerlo porque evita confundir cosas distintas bajo el mismo nombre:

  • Atómica vs. holística. Una verifica una regla aislada (“este módulo no importa a aquel”); la otra mide una propiedad emergente de varias partes interactuando.
  • Disparada vs. continua. Una corre ante un evento (un commit, el pipeline de CI); la otra monitorea sin parar (un presupuesto de latencia vigilado en producción).
  • Estática vs. dinámica. Una da un resultado fijo (pasa o falla); la otra depende de datos de ejecución (un umbral que se ajusta con la carga).

La mayoría de las fitness functions de arquitectura, las que protegen capas, dependencias y fronteras, son atómicas, disparadas y estáticas: baratas, corren en CI, no necesitan infraestructura. Los presupuestos de rendimiento y los SLOs viven en el extremo dinámico y continuo. Empezar por las primeras da el mayor retorno con el menor costo.

Por qué importan

Tres razones, en orden de impacto:

El drift se detecta en CI, no en producción. La diferencia entre descubrir una violación de capas en el pull request y descubrirla en una auditoría seis meses después es la diferencia entre borrar una línea y reescribir un subsistema.

Es documentación que no puede mentir. Un comentario o un diagrama pueden quedar desactualizados sin que nadie lo note. Una fitness function no: o la regla es cierta, o el build está rojo. Es una especificación ejecutable. El diagrama de arquitectura deja de ser una aspiración y se vuelve una afirmación verificada.

Habilitan evolucionar sin miedo. Este es el corazón de la arquitectura evolutiva. Puedes refactorizar de forma agresiva porque las barreras gritan en el instante en que cruzas una. La red de seguridad no es la cobertura de tests de negocio; es el conjunto de reglas estructurales que el sistema se niega a violar.

Aurora: la teoría hecha test

Toda esta teoría se ve abstracta hasta que se aterriza. Aurora es un ERP multi-tenant para contabilidad: un kernel contable y un conjunto de módulos satélite que se distribuyen como paquetes. El modelo de construcción es “equipo-como-fábrica”: los módulos nacen de un manifiesto y se multiplican rápido. Justamente por eso, la arquitectura no puede depender de que cada desarrollador recuerde las reglas. Las reglas tienen que ser tests.

Estas son algunas de las invariantes que no pueden romperse, y que en Aurora viven como fitness functions en tests/Arch/:

  • El kernel contable no depende de ningún otro módulo. Es la regla de gravedad: todo orbita el libro mayor (el GL), y nadie escribe en journal_lines por la puerta de atrás.
  • Los paquetes de Aurora nunca importan App\Modules. Hablan con el host solo a través de las costuras del SDK. Es la frontera que mantiene a los módulos desacoplados del núcleo.
  • El dinero es BCMath, nunca float. Una regla de corrección numérica, no de estilo.
  • La superficie del SDK está congelada. Un cambio de firma falla ruidosamente en CI, en vez de romper N módulos en silencio.

El siguiente diagrama muestra la regla de gravedad, la invariante estrella, y qué fitness function vigila cada frontera:

Sales CRM Deferrals Core Accounting · kernel (GL) nadie escribe journal_lines directo depende de depende de depende de App\Modules Capas (Sales/CRM/Deferrals → Core → kernel): ArchitectureTest Frontera de paquetes: PackageBoundaryTest
La regla de gravedad de Aurora. Las flechas permitidas las vigila ArchitectureTest; la frontera que prohíbe a un paquete tocar el núcleo, PackageBoundaryTest.

¿Cómo se ve una de estas en código? En Aurora se escriben con Pest Arch, y leen casi como la regla en español:

arch('Accounting es el kernel: no depende de ningún otro módulo')
    ->expect('App\Modules\Accounting')
    ->not->toUse('App\Modules\FixedAssets');

No hay más magia. Es un test. Corre en la misma suite que el resto, en paralelo, sin base de datos. Y el día que alguien haga que el kernel importe otro módulo, ese test, no una persona seis meses después, será quien lo detenga.

El payoff a escala

Sin estos tests, “el kernel no depende de nadie” es una esperanza. Con ellos, es una propiedad que CI demuestra en cada PR. Cuando un módulo generado por la fábrica accidentalmente mete la mano en el núcleo, el build se pone rojo antes de la revisión. La arquitectura sobrevive a su propio crecimiento.

El caso más nítido es el contrato del SDK. Aurora congela la superficie de sus contratos con una fitness function (PlatformContractsFreezeTest): cambiar una firma rompe un test, ruidosamente, en lugar de romper N paquetes en silencio y descubrirlo en producción. Esa es la economía de fondo: un detector barato en CI evita un incidente caro en el mundo real.

Cómo empezar

No hace falta un libro entero para arrancar. Tres pasos:

  1. Identifica tu invariante #1: la regla que, si se rompe, más duele. Casi siempre es una de estas: una capa que no debe invertirse, una dependencia prohibida, un tipo de dato para el dinero, una convención de nombres con consecuencias.
  2. Escríbela como test. Una sola. Que falle si la regla se viola hoy a propósito (verifica que el detector funciona).
  3. Ponla en CI como barrera bloqueante. Si no bloquea el merge, es una sugerencia, no una invariante.

Las herramientas existen para casi todo stack: Pest Arch (PHP), ArchUnit (Java), NetArchTest / ArchUnitNET (.NET), dependency-cruiser o reglas de fronteras en ESLint (JS/TS), import-linter (Python). El concepto es el mismo en todos; cambia la sintaxis.

Cierre

Las fitness functions cambian la naturaleza de la arquitectura: deja de ser un diagrama que esperamos que refleje la realidad y se vuelve una propiedad que probamos. Son la forma en que un sistema se gana el derecho a evolucionar sin volverse frágil.

En Aurora, cada módulo nuevo deja la plataforma más capaz sin dejarla más quebradiza, porque las reglas que importan no son aspiraciones escritas en un documento sino tests que corren en cada cambio. Esa es, al final, toda la idea: la arquitectura que no se prueba, se degrada.