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.
StoreDomaingestión de pedidos
BillingDomainfacturación
MessagingDomainnotificaciones
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 →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.
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.
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.
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.
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ó.
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.
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).
