Diseña el modelo, luego el código.

Un modelo de datos suele empezar por un ORM. Pero un ORM describe no el dominio del problema sino la base de datos construida sobre él; para NoSQL no llega a existir ningún modelo de entidades. AOA describe el dominio tal como es, igual para SQL que para NoSQL.

Una base de datos se construye para almacenar de forma fiable y leer rápido, no para comprender: primero la normalización, luego la desnormalización por velocidad, y el dominio queda distorsionado dos veces, en direcciones opuestas. Aquí, en cambio, se construye por pasos: primero el mapa de dominios, luego las entidades que lo llenan, las relaciones entre ellas y un lifecycle para cada una. Todo ello se empaqueta en un Resource: a través de él, una base de datos real se lee dentro del modelo.

Documentación ↗

El mapa de dominios

Conocer un sistema nuevo siempre empieza con una pregunta: para qué existe y qué hace. Son preguntas sobre el sentido, no sobre la construcción. Pero la primera respuesta suele ser la infraestructura: controladores, servicios, repositorios, migraciones. El dominio del problema no se distingue detrás de ellos.

El mapa de dominios es la respuesta a esa primera pregunta. En él solo están las áreas de las que se compone el sistema: StoreDomain — gestión de pedidos, BillingDomain — facturación, MessagingDomain — notificaciones. Una respuesta sin detalles abarca el sistema entero y cabe en la cabeza: el primer punto de apoyo.

El mapa de dominios
Documentación completa ↗

Las entidades expresan el dominio

Un dominio tiene dos cosas principales: Entities — pedido, cliente, línea — y Actions, las operaciones con nombre que actúan sobre ellas. Ni controladores, ni tablas, ni el resto de la infraestructura. Eso permite bajar un paso más sin perder el foco en la intención de negocio del sistema, en lugar de en su implementación técnica.

Una Entity es una clase corriente: un nombre, campos tipados y nada más. No sabe nada de un ORM, nada de tablas, nada de dónde vendrán los datos: su única tarea es describir un objeto del mundo real tal como se habla de él: un pedido tiene un total y una moneda, un cliente tiene un nombre y un correo. Los campos son de dos clases: simples — cadena, número, fecha — y referencias a otras Entities, porque un pedido tiene un cliente y tiene líneas. Son las segundas las que convierten un conjunto de clases sueltas en un modelo, y de ellas trata la sección siguiente. El comportamiento, en cambio, no está: una Entity solo describe, y todo lo que se hace con el objeto vive en un Action.

Qué es un Action →
CustomerEntityPKidstrnamestremailstrOrderEntityPKidstrtotalfloatcurrencystrstatusstr
Documentación completa ↗

Las relaciones explicitan la propiedad

Ya está claro de qué se compone un dominio. Pero ¿qué lo mantiene unido y qué impide que sus relaciones se desajusten?

Una relación se describe con dos cosas: con cuánta fuerza se sujetan los objetos entre sí — composición, agregación o asociación — y cuántos objetos hay a cada lado, uno o varios. Cada entidad declara la relación entera: su propio lado y el opuesto, con tipo y cardinalidad en ambos. Por eso basta mirar una entidad para ver de golpe todas sus relaciones. Al arrancar, el sistema coteja cada par: si los dos lados no coinciden, el arranque se detiene. A veces no hay segundo lado: la relación mira más allá de su propio almacén. También se puede, pero hay que declararlo.

OrderEntityPKidstrtotalfloatcurrencystrstatusstrFKcustomer→ CustomerEntityFKlines→ OrderLineEntityCustomerEntityPKidstrnamestremailstrFKorders→ OrderEntityOrderLineEntityPKidstrskustrFKorder→ OrderEntity
Documentación completa ↗

Lifecycle controla cada transición

Mientras el estado sea solo una cadena, nada impide que un pedido salte de borrador a entregado. Lo que está permitido y lo que no vive en la cabeza del equipo, o en un comentario que quedó obsoleto hace mucho.

En AOA esas reglas dejan de ser orales: los estados de un pedido y las transiciones entre ellos se declaran junto a la propia entidad, como una máquina de estados — borrador, pagado, enviado, entregado —, y la cancelación solo es posible desde los dos primeros. No es una recomendación: una transición que no está en la lista no existe, y un pedido no puede acabar entregado sin haber sido pagado. La máquina misma se comprueba al arrancar: un estado al que no se puede llegar, o una transición a ninguna parte, detienen el arranque mucho antes de que pase el primer pedido real.

La máquina se declara por sí sola: qué estados puede tomar un pedido y qué transiciones entre ellos están permitidas. Una vez, aparte de cualquier entidad.

draftpaidshippeddeliveredcancelled

Ahora se puede aplicar: en OrderEntity el campo status deja de ser una cadena y pasa a tener el tipo OrderLifecycle. Una cadena aceptaba cualquier valor; este tipo solo los que tienen un camino hasta ellos.

OrderEntityPKidstrtotalfloatcurrencystrlifecycleOrderLifecycle
Documentación completa ↗

Resources definen la frontera del almacenamiento

El modelo describe el dominio del problema, pero no trabaja con bases de datos directamente: no debe depender de cómo estén hechas. Así que otro tiene que traer los datos de una base real — y devolverlos.

De eso se encarga un Resource. Guarda todo lo que debe vivir entre llamadas: la conexión, el pool, el cliente. Su tarea es abrir, ejecutar y devolver; dentro no hay reglas de negocio. Lo que cruza esa frontera son las entidades. Un mismo modelo puede estar representado a la vez por varios Resource: uno lee de SQL, otro de NoSQL, un tercero del servicio HTTP de otro, y todos devuelven el mismo OrderEntity. El código de la lógica de negocio nunca sabe cuál de ellos actuó: ni el nombre de la tabla, ni el dialecto, ni el formato de la respuesta llegan hasta él. Por eso cambiar de almacenamiento significa cambiar solo el Resource.

códigoResourceOrderEntityNoSQLSQLservicio HTTPuna interfaz · misma OrderEntity · fuente intercambiable
Tres tipos de Resource: Storage, Gateway, Controller ↗Documentación completa ↗

Las proyecciones dan forma a cada lectura

La misma entidad se lee de la base a veces entera, a veces parcialmente, a veces solo como un identificador. La respuesta habitual es un zoo de DTO, una clase por cada caso.

El enfoque habitual es un DTO propio para cada método del Resource. A diferencia de devolver diccionarios corrientes, eso da protección estática frente a erratas en los nombres de los campos. Pero el enfoque tiene otra cara: la entidad original se deshace en un montón de DTO pequeños, y el modelo del dominio sobrevive solo en la documentación y en diagramas que quedan obsoletos antes de la primera versión.

La idea de AOA es no renunciar al modelo entero: un método del Resource devuelve los propios objetos del modelo, tal como están declarados. Los campos se leen por atributos con nombre, igual que en un DTO, pero no aparecen clases nuevas, y meter en el modelo un campo que el modelo no tiene se vuelve imposible. Su vigencia la confirma el mismo código que lo usa, y el ERD se dibuja directamente a partir de él.

Por eso se introduce la noción de proyección de datos: cuando un método del Resource lee la entidad de forma parcial, devuelve esa misma entidad, pero los campos que no se leyeron de la base quedan marcados como no disponibles. Al primer intento de usarlos el sistema lanza una excepción clara en vez de callar.

El modelo ERD en Maxitor →

get_order(order_id) → OrderEntity

El caso obvio — la entidad entera, todos los campos cargados; la clase del modelo usada exactamente como se declaró.

OrderResourceget_order()find_orders()get_full_order_info()OrderEntityPKid"ORD-1024"total149.90currency"RUB"status"paid"devuelve

find_orders(query) → list[OrderEntity]

Una colección esbelta — solo se carga el id de cada pedido; leer cualquier otro campo levantaría un error.

OrderResourceget_order()find_orders()get_full_order_info()OrderEntityPKid"ORD-1024"totalcurrencystatusdevuelve

get_full_order_info(order_id) → OrderEntity

El caso profundo — el pedido más sus entidades anidadas, cada una cargada hasta una profundidad parcial diferente (el id siempre se carga, es la clave).

OrderResourceget_order()find_orders()get_full_order_info()OrderEntityPKid"ORD-1024"total149.90FKcustomer→ CustomerEntityFKlines→ OrderLineEntityCustomerEntityPKid"CUST-7"name"Ivan Petrov"emailOrderLineEntityPKid"LN-1"sku"SKU-55"quantity3unit_pricedevuelve
Documentación completa ↗
Diseña el modelo, luego el código. · aoa.run