Siete carpetas y una tarde de lluvia

Una tarde de lluvia en Medellín, mientras intentaba rastrear un bug que afectaba tres partes distintas del sistema, me quedé mirando la estructura de directorios en mi editor. Tenía un cambio en un parser lógico elemental (Autologic), que requería una actualización en el compilador de nuestro lenguaje educativo (ST), lo que a su vez modificaba la respuesta del Backend en la nube (AgoraBack), terminando finalmente en una actualización visual en la interfaz del usuario (AgoraFront).

La solución rápida, la que recomiendan los tutoriales de moda, era obvia: meterlo todo en un monorrepo. Un solo repositorio de Git, un solo historial, un solo commit que lo cambiara todo a la vez. Pero al ver la realidad del despliegue de cada pieza, la idea de unificar el control de versiones se sintió más como una capitulación que como una solución.

¿Por qué? Porque el frontend se despliega de forma automática en Vercel. El backend corre en contenedores de Cloud Run. El Hub central interactúa directamente con el sistema operativo de un VPS a través de un servicio systemd. El Worker se empaqueta en imágenes Docker independientes. La CLI y las librerías de lógica y de lenguaje son paquetes npm públicos que otros desarrolladores instalan de forma independiente.

Meter todo eso en un solo repositorio Git con un único pipeline de integración continua habría sido meter en una licuadora procesos que tienen velocidades y naturalezas completamente distintas. Forzar a una librería TypeScript pura a compartir ciclo de vida con un demonio de Linux en Bash es violencia arquitectónica.

Ahí nació la pregunta que dio origen a agora-workspace: ¿Cómo se coordina el desarrollo de lo que no cabe en un solo repositorio sin perder la cordura en el intento?

El laberinto del acoplamiento invisible

Cuando empezamos a construir sistemas distribuidos, solemos caer en una falsa dicotomía. Nos dicen que debemos elegir entre el monorrepo —donde todo vive junto y comparte destino— y el polyrepo —donde cada pieza vive en su propia isla remota—.

El polyrepo clásico es una pesadilla de desarrollo local. Si trabajas con siete repositorios independientes, hacer un cambio transversal implica clonar siete cosas, lidiar con comandos de enlace local como npm link que se rompen al menor suspiro del sistema operativo, y cambiar de ventana constantemente. Terminas con la sensación de estar manejando siete proyectos ajenos en lugar de construir una sola idea.

Pero el monorrepo clásico esconde una trampa peor: el acoplamiento invisible. Cuando todo es fácil de importar porque está a un directorio de distancia, las fronteras lógicas empiezan a disolverse. Es la ley del mínimo esfuerzo. Si necesitas una función de utilidad en el frontend y está definida en el backend, la importas directamente. Total, el compilador no se queja y el repositorio es el mismo. Con el tiempo, tu arquitectura se convierte en una bola de barro donde no puedes actualizar una dependencia en el backend sin romper el build de la interfaz de usuario.

Queríamos coherencia, pero no acoplamiento. Queríamos que las siete piezas se sintieran como un solo proyecto sobre el escritorio de trabajo, pero que mantuvieran su independencia absoluta una vez que salieran de él.

El meta-repositorio o el tercer camino

La respuesta no fue buscar una nueva herramienta de orquestación compleja que agregara otra capa de abstracción al problema. Fue volver a lo básico y diseñar un meta-repositorio. Lo llamamos agora-workspace.

Es una carpeta raíz que no contiene código de negocio. No hay ahí lógica de base de datos ni componentes de interfaz. Contiene, simplemente, el pegamento.

Ese pegamento se compone de tres cosas: un package.json raíz configurado con npm workspaces, un archivo docker-compose para levantar las bases de datos y los servicios locales en un par de segundos, y un conjunto de scripts de humo (smoke tests) que validan que el cableado entre las partes siga funcionando.

Dentro de esta carpeta raíz, clonamos los siete repositorios independientes. Cada uno conserva su directorio .git/ propio, sus ramas, su historial y sus pull requests. Para Git, son siete proyectos soberanos. Para el editor de código y el gestor de paquetes local, es un único espacio de trabajo coordinado.

Si modifico el parser de Autologic o el compilador de ST, el entorno de desarrollo local resuelve la dependencia usando la copia local que está en la carpeta de al lado, no la que está publicada en el registro de npm. Puedo probar el impacto de mi cambio en el backend y en el frontend en tiempo real, sin publicar versiones previas ni hacer malabarismos de red. Pero cuando llega el momento de confirmar el cambio, cada pieza se sube a su propio repositorio y sigue su propio camino de integración continua hacia su servidor correspondiente.

Es una estructura híbrida. Coherencia para el humano que escribe; independencia para la máquina que despliega.

La fricción como diseño

Llevamos años obsesionados con eliminar la fricción en el desarrollo de software. Nos venden la idea de que cualquier obstáculo entre una idea y su puesta en producción es un fallo que debe ser corregido. Queremos que todo sea fluido, inmediato, invisible.

Sin embargo, trabajar en este espacio intermedio me enseñó que la fricción no siempre es un bug; a veces es un rasgo de diseño esencial.

En agora-workspace, si quiero que un cambio en la lógica del lenguaje ST impacte en el backend, estoy obligado a hacer cosas molestas: tengo que hacer un commit en el repositorio de ST, abrir un pull request, esperar a que pase sus pruebas automáticas, y luego actualizar de forma explítica la versión del paquete en el backend.

Esa molestia no es gratuita. Es una pausa obligada. Es el momento en el que el diseño del sistema me fuerza a detenererme y pensar: "¿Este cambio realmente pertenece al compilador del lenguaje, o estoy metiendo atajos específicos del backend dentro de una librería de propósito general?".

Si el sistema hubiera sido un monorrepo puro, ese límite no habría existido. Habría modificado el archivo directamente y lo habría subido en un solo commit mixto. Habría ahorrado diez minutos de mi tarde a cambio de sembrar una deuda técnica que alguien tendría que pagar dos años después.

La fricción física de cambiar de repositorio y de flujo de Git actúa como un amortiguador contra las malas decisiones de diseño. Nos obliga a respetar las interfaces y los contratos que definimos entre los componentes. En software, como en la vida, cuando la comunicación se vuelve demasiado fácil, la conversación suele perder calidad.

La ilusión de la unidad

Esta paradoja de la coordinación no es exclusiva de la informática. Es el problema eterno de cualquier sistema complejo, desde una cooperativa de trabajadores hasta un organismo vivo.

Queremos la ilusión de la unidad. Nos gusta pensar en el software que construimos como en un todo coherente y monolítico, pero la realidad física y operativa es que siempre estamos lidiando con fragmentos. Ningún sistema grande es una sola cosa; es una federación de partes semi-autónomas que acuerdan colaborar bajo ciertas reglas.

Forzar la unidad mediante el acoplamiento técnico es una forma de autoritarismo arquitectónico que termina en fragilidad. Si el frontend no puede compilar porque el pipeline de docker-compose del worker falló en una prueba de integración, hemos creado un monstruo rígido que no puede adaptarse a la velocidad de sus partes.

Pero la fragmentación absoluta tampoco es la respuesta. No podemos dejar que cada microservicio o librería se convierta en una provincia aislada con sus propias reglas y herramientas, porque entonces el costo de cruzar la frontera se vuelve prohibitivo y los desarrolladores terminan agotados por la burocracia de su propio sistema.

El meta-repositorio es nuestra forma de aceptar la complejidad sin intentar domesticarla a la fuerza. No unifica las partes; unifica el espacio donde se encuentran.

Al final, construir software no consiste en buscar la estructura perfecta ni en seguir ciegamente el último dogma de la industria. Consiste en diseñar los espacios intermedios. Consiste en saber cuánta distancia y cuánta fricción necesitamos mantener entre las cosas para que puedan trabajar juntas sin terminar destruyéndose las unas a las otras.

— Steven Vallejo, Medellín