
No necesitas un diseño perfecto desde el principio. Necesitas una dirección y las ganas de seguir refinando hasta que las cosas converjan.
Esto es lo que aprendí después de construir la misma aplicación una docena de veces, quemarla y reconstruirla, hasta que el sistema me enseñó lo que quería ser.
Todo proyecto de software empieza igual: alguien dibuja cajas en una pizarra y declara: «Esta es la arquitectura». Suele haber muchos asentimientos. A veces hay una segunda pizarra.
Entonces empiezas a construir de verdad, y la realidad aparece sin que nadie la invite.
Las cajas no encajan. Los datos fluyen en la dirección equivocada. Ese servicio que creías sencillo tiene diecisiete casos límite. Hacia la tercera semana estás escribiendo código que pide disculpas en voz baja a la pizarra.
La dura verdad es esta: no puedes conocer la forma completa de tu sistema antes de haber construido partes de él. Los requisitos mienten. Los usuarios te sorprenden. La escala revela cosas que ningún diagrama predijo.
Y eso está bien. El problema no es que tu diseño estuviera equivocado. El problema es tratar el diseño original como sagrado.
Si has trabajado con sistemas distribuidos, conoces el concepto de consistencia eventual: distintos nodos de un sistema pueden tener vistas diferentes de los datos en un momento dado, pero con el tiempo y la comunicación suficientes, todos convergen al mismo estado.
El diseño de tu software funciona igual.
Empiezas con información incompleta. Tu arquitectura tiene huecos. Tu comprensión del dominio es parcial. Pero si sigues iterando, y cada ciclo hace el diseño un poco más honesto, un poco más alineado con la realidad, convergerás hacia algo que de verdad encaja.
El objetivo no es acertar a la primera. El objetivo es seguir equivocándote cada vez menos.
Llámalo desarrollo Figure-It-Out-As-You-Go (FIAYG), es decir, «ya lo averiguarás sobre la marcha». No es tan pegadizo como TDD o DDD, pero es bastante más honesto sobre cómo se construye realmente el software.
La clave es esta: cada vez que aprendes algo nuevo, de un error, del feedback de los usuarios, de chocar con un muro de escala, codificas ese aprendizaje en la arquitectura. No arreglas solo el síntoma. Arreglas el sistema para que el síntoma no pueda repetirse.
Almadar es un marco declarativo que compila esquemas de máquinas de estados en aplicaciones full-stack. Un solo archivo .orb describe tus entidades, comportamientos e interfaz, y el compilador genera un frontend en React que funciona, un backend en Express y los modelos de la base de datos.
Eso es lo que es ahora. Así llegamos hasta aquí.
Empezó de forma sencilla. Teníamos un proyecto, un cliente que necesitaba una aplicación de negocio con mucho CRUD. La programamos a mano, de la forma normal: componentes de React, rutas de Express, entidades de TypeORM. Código repetitivo por todas partes.
Notamos que escribíamos los mismos patrones una y otra vez: una tabla para listar registros, un modal para crearlos, un formulario para editarlos y una confirmación para borrarlos. Copiar. Pegar. Renombrar. Repetir.
Tiene que haber una forma mejor.
Así que probamos algo: ¿y si describíamos los datos y el comportamiento en un esquema JSON y generábamos el código repetitivo a partir de él?
Fue tosco. El esquema apenas tenía estructura. El generador era una plantilla de cadenas con llamadas a fs.writeFileSync. Pero funcionó. El cliente estaba contento. Entregábamos más rápido.
Lo que aprendimos: el 80 % de toda aplicación que es código repetitivo se puede describir de forma declarativa. El 20 % que es único es en lo que de verdad quieres invertir tu tiempo.
El segundo cliente tenía requisitos distintos. Nuestro generador estaba demasiado acoplado a los patrones del primer cliente. Habíamos fijado suposiciones por todas partes: convenciones de nombres, estructura de archivos, jerarquía de componentes.
Podríamos haberlo parcheado. En cambio, nos detuvimos y preguntamos: ¿Cómo sería una versión general de esto?
Aquí la arquitectura empezó a cristalizar. Introdujimos el concepto de Orbitals, módulos de funcionalidad autocontenidos que combinan una entidad (la forma de los datos), un trait (comportamiento como máquina de estados finitos) y una página (la ruta). Separamos la definición del esquema de la generación de código.
Lo que aprendimos: en el momento en que tienes un segundo caso de uso, tus abstracciones se revelan. Si no se generaliza, no era una abstracción. Era una coincidencia.
A medida que crecía el número de proyectos, crecía también el caos. Cada proyecto de cliente tenía su propio sistema de diseño, sus propias variaciones de componentes, su propia idea de cómo era un «botón». Manteníamos diez sabores del mismo componente.
Así que reestructuramos todo en un monorepo con reglas explícitas:
@almadar/ui, compartidos, versionados, nunca duplicadosprojects/{name}/design-system/, extienden el núcleo, no lo reemplazanorbital-rust/, sagrado, nunca se modifica sin discutirloprojects/{name}/app/, nunca se edita directamenteMás importante aún, añadimos restricciones. Reglas duras que el equipo codifica en un archivo CLAUDE.md y que todo desarrollador (humano y de IA) debe seguir:
.orb, recompila, nunca toques el código generadoY hicimos cumplir esas restricciones con un plugin de ESLint propio (@almadar/eslint). No eran pautas opcionales, sino errores de verdad que bloquean el hook de pre-commit. Cero advertencias permitidas. Algunas de las reglas que construimos:
| Regla | Qué detecta |
|---|---|
no-raw-dom-elements | Bloquea <div>, <span>, <button>, etc. Usa VStack, Typography, Button en su lugar |
no-as-any | Bloquea los casts de tipo as any. Arregla el problema de tipos real |
no-import-generated | Bloquea importar desde projects/*/app/. El código generado no es una API |
require-closed-circuit-props | Todo componente debe aceptar className, isLoading, error, entity |
require-event-bus | Las interacciones deben usar useEventBus(), no onClick en bruto |
require-display-name | Todo componente debe definir displayName para depurar |
require-stories | Todo componente nuevo debe entregarse con un archivo .stories.tsx |
event-naming-convention | Los eventos de la UI deben seguir el patrón UI:EVENT_NAME |
template-no-hooks | Los componentes plantilla son puros. Sin useState, sin useEffect |
template-no-iteration | Las plantillas no hacen bucles. La iteración la hace el organismo |
organism-no-callback-props | Los organismos se comunican por el bus de eventos, no con callbacks en las props |
El hook de pre-commit ejecuta ESLint sobre cada archivo .ts/.tsx preparado. Si salta cualquier regla, el commit se bloquea. Sin excepciones. Toda regla tiene severidad 2 (error), no advertencia. Las advertencias son aspiracionales. Los errores son arquitectura.
El resultado: un desarrollador nuevo, humano o de IA, no puede violar por accidente los principios de diseño. El linter dice que no antes de que se abra siquiera la pull request.
Las restricciones suenan limitantes. En realidad liberan. Cuando las reglas son claras, las decisiones se toman más rápido, las inconsistencias se detectan antes y los miembros nuevos del equipo pueden contribuir sin romper nada.
Lo que aprendimos: un monorepo sin restricciones es solo un monolito educado. Las restricciones son la arquitectura, y el linting es cómo las haces reales.
Aquí es donde se pone interesante.
Cuando llegaron los asistentes de programación con IA, nuestra arquitectura basada en restricciones resultó ser exactamente lo que necesitaban para trabajar bien. ¿Un modelo de lenguaje grande (LLM) con un esquema claro que editar, un registro de patrones contra el que validar, un linter que bloquea los malos patrones y un compilador que dice «no» a la salida rota? Eso es un agente de IA productivo.
¿Un LLM trabajando en una base de código libre y sin reglas? Eso es un agente muy seguro de sí mismo que hace lo incorrecto con total seguridad.
Lo formalizamos: la IA recibe las restricciones en CLAUDE.md, consulta el registro de patrones antes de añadir componentes, ejecuta ESLint sobre cada archivo que cambia y valida el esquema con el compilador de Orbital antes de dar nada por terminado. El compilador es el revisor de código de la IA, nunca se cansa, nunca se le pasa un caso y se ejecuta en menos de 50 ms.
La arquitectura con la que tropezamos a base de iterar resultó ser la arquitectura correcta para el desarrollo asistido por IA. No lo planeamos. Convergimos hacia ella.
Lo que aprendimos: la IA amplifica cualquier estructura (o caos) que ya exista en tu base de código. Las restricciones no son burocracia. Son la diferencia entre una IA que ayuda y una IA que rompe cosas con total seguridad.
Si eres principiante, así puedes aplicar esto sin construir un marco desde cero:
Tu primera versión debe hacer una sola cosa. No la cosa, más manejo de errores, más caché, más un sistema de configuración. Solo la cosa. Aprenderás más entregando algo pequeño que diseñando algo grande.
Cuando escribes el mismo código por tercera vez, la base de código te está diciendo que algo quiere abstraerse. No abstraigas en la primera repetición (prematuro). No ignores la tercera (negligencia). La segunda vez es la señal, la tercera es la confirmación.
Cuando encuentres un patrón de error, como un modal que puede quedarse abierto atascado, no arregles solo ese modal. Averigua qué regla evitaría por completo esa clase de error. Escríbela. Hazla cumplir. En el caso de Almadar, convertimos «todo modal debe tener una transición CANCEL» en una regla de validación del compilador, y «nunca uses elementos HTML en bruto» en un error del linter. En tu proyecto puede ser una regla de ESLint propia, un hook de pre-commit o una lista de revisión de código. El medio no importa. Lo que importa es que la regla sea automática. Si requiere que un humano la recuerde, acabará olvidándose.
Cada vez que detectas un patrón y lo generalizas, deberías escribir menos código para lograr lo mismo. Si tu base de código sigue creciendo al mismo ritmo para siempre, estás acumulando complejidad, no gestionándola. El objetivo es más funcionalidad por línea, no más líneas.
Renombramos cosas. Borramos paquetes enteros. Movimos el sistema de diseño dos veces. Cada vez que lo hicimos, el sistema quedó más limpio y las reglas más claras. Un giro no es un fracaso. Es la prueba de que tu comprensión ha mejorado.
Después de varios años y muchos proyectos de clientes, esto es a lo que convergimos:
La mejor arquitectura de software no es la que diseñas antes de empezar. Es la que surge de escuchar lo que tu sistema intenta decirte, a través de los errores, de la repetición, de la fricción que sientes cuando algo va mal.
Averígualo sobre la marcha. Codifica lo que aprendes. Sigue equivocándote cada vez menos.
La consistencia es eventual. Entrega de todos modos.