03_Punto-de-Partida/01 -- Módulo de descripciones, backend.md

El backend de DWall está construido sobre Spring Boot 2.7 con Java 21. Parte del equipo de desarrollo aplica principios de Domain-Driven Design: los módulos están organizados como bounded contexts independientes, la comunicación entre ellos se realiza mediante eventos de dominio, y la lógica de negocio sigue una arquitectura hexagonal, terminos que se explicaran a fondo maás adelante.

Durante el desarrollo de prácticas, se implementó el módulo de descripciones, el cual fue diseñado siguiendo estas mismas conveciones. Este mismo módulo es el responsable principal en nutrir al agente RAG de la información fundamenta sobre los diferentes recursos (variables, reglas, consultas, etc.) que se pueden definir en Xabet.

Al ser un aspecto diseñado para ser totalmente opcional para todas las marcas y al estar orientado a ser característico de un módulo RAG, el diseño orientado a dominios determina que debería tener su propio bounded context o módulo en DWall. Por lo que se decidió que cada uno de los recursos tiene su propio su representante dentro del módulo de descripciones.

Todos estos conceptos — arquitectura hexagonal, bounded contexts, eventos de dominio — se mencionan aquí de forma introductoria, ya que en el punto de partida el alumno los aplicó siguiendo las convenciones del proyecto sin necesidad de comprenderlos en profundidad. Será en el capítulo de desarrollo donde se analizarán con rigor, puesto que la implementación de las nuevas funcionalidades del TFG exigió una investigación mucho más profunda de estos principios.

Tipos de entidades

El módulo de descripciones opera sobre un conjunto cerrado de tipos de entidad: los recursos del dominio de DWall (variables, reglas, etiquetas, usuarios, etc.) a los que un operador puede asignar una descripción textual. A continuación se describen estos tipos y cómo se representan en el esquema de base de datos. Las relaciones entre las tablas resultantes se analizarán en detalle en el siguiente subcapítulo.

Catálogo de entidades: module_description_entities

Para comprender dichos recursos — las entidades del dominio de DWall que el módulo de descripciones puede describir — a continuación aparece la primera de las tablas. La tabla module_description_entities actúa como catálogo estático de todos los tipos de recurso soportados. Cada fila registra un tipo, y su id es el que se referencia como entity_id en el resto del esquema.

identityDescripción del recurso
1VARIABLEVariable nativa de DWall que representa una señal de planta
2AGGREGATEVariable agregada, con una fórmula matemática asociada
3TAGEtiqueta de agrupación o clasificación de recursos
4QUERYConsulta guardada definida por el usuario
5USERUsuario de la plataforma Xabet
6RULERegla de negocio o condición de incidencia
7GROUPGrupo de usuarios
8FILEArchivo o documento adjunto

Tabla central: module_description_description

Las descripciones en sí funcionan mediante un patrón mirror. La tabla module_description_description es el núcleo del módulo: almacena el texto de la descripción junto con los metadatos que la contextualizan.

CampoTipoDescripción
idbigserialClave primaria autoincremental
entity_idbigintFK → module_description_entities.id (tipo de recurso)
type_idbigintID del recurso concreto en su módulo de origen
descriptiontextTexto de la descripción
user_idbigintFK → module_description_user.id (quién creó la descripción)
timestamptimestampFecha de última modificación (por defecto now())

Una constraint UNIQUE(entity_id, type_id) garantiza que solo puede existir una descripción por cada par recurso-tipo, es decir, una descripción por entidad concreta.

Ejemplo de datos:

identity_idtype_iddescriptionuser_idtimestamp
1142"Temperatura del horno principal en grados Celsius"72025-06-01 10:23:00
2315"Etiqueta para agrupar sensores de la línea 3"72025-06-01 10:30:00
348"Consulta de consumo energético diario por turno"122025-06-02 09:00:00
463"Regla que detecta temperatura crítica en hornos"122025-06-03 14:10:00

El Patrón Mirror

La restricción fundamental de un diseño orientado a dominios es que el módulo de descripciones no puede hacer JOINs contra tablas de otros bounded contexts. Sin embargo, para poder operar de forma autónoma, el módulo necesita conocer el nombre legible del recurso que describe — por ejemplo, para mostrar "Variable: T_HORNO_PRINCIPAL" en vez de "entity_id: 42".

La solución implementada es el patrón mirror: por cada tipo de recurso existe una tabla espejo dentro del propio módulo de descripciones que replica únicamente los campos mínimos necesarios (el ID y el nombre legible del recurso). Estas tablas no son la fuente de verdad — son proyecciones locales que se mantienen en sincronía con su módulo de origen.

El id de cada tabla mirror se corresponde con el type_id de la tabla central module_description_description. Para que un recurso aparezca en el mirror, su descripción debería existir previamente en la tabla central, por lo que se genera una Foreign Key lógica.

  Módulo de Variables                Módulo de Descripciones
  ──────────────────                 ───────────────────────────────────
  ┌───────────────┐                  ┌─────────────────────────────────┐
  │   variable    │──(evento)──────▶ │ module_description_variable     │
  │   id: 42      │                  │   id: 42                        │
  │   name:       │                  │   name: T_HORNO_PRI             │
  │   T_HORNO_PRI │                  └─────────────────────────────────┘
  └───────────────┘                                 │
                                                    │ type_id = 42, entity_id = 1
                                                    ▼
                                    ┌─────────────────────────────────┐
                                    │ module_description_description  │
                                    │   entity_id: 1 (VARIABLE)       │
                                    │   type_id: 42                   │
                                    │   description: "Temperatura..." │
                                    └─────────────────────────────────┘

Cada tabla mirror almacena el identificador del recurso y su nombre o referencia legible. Por ejemplo, la tabla que replica las variables nativas:

module_description_variable

Refleja las variables nativas del módulo de variables.

idname
42T_HORNO_PRINCIPAL
43P_LINEA_3

Este mismo patrón se repite para todos los casos. Lo que consecuentemente genera una estructura final de la siguiente forma:

Las relaciones sólidas son FKs reales definidas en el esquema. Las punteadas representan la relación lógica entre type_id y cada tabla mirror: no hay FK porque type_id puede apuntar a cualquiera de ellas dependiendo del entity_id, por eso el módulo necesita el patrón mirror en lugar de un JOIN directo.

Flujo del sistema

A continuación se analiza el flujo interno del módulo de descripciones. El ejemplo utilizado es el caso de uso de guardar una descripción, aunque el mismo esquema se aplica a las operaciones de edición y eliminación: la actualización del texto se gestiona de forma síncrona a través del API, mientras que cualquier cambio en el nombre del recurso asociado se propaga mediante eventos de dominio.

Flujo interno del módulo

Analizando en detenimiento el funcionamiento del backend en java, el API del módulo se expone a través de DescriptionController, que atiende las peticiones REST bajo /api/web/description. El controller delega toda la lógica en DescriptionService, que actúa como orquestador.

// DescriptionService.java
public Long saveDescription(final Long typeId, final String entity, final String descriptionText) {
    DescriptionEntity descriptionEntity = getDescriptionEntity(entity);
    String username = accountService.getAccount().getUserId();
    Description description = new Description(typeId, descriptionEntity.getId(), descriptionText, userId);
    return descriptionRepository.save(description);
}

La operación principal es saveDescription: recibe el ID del recurso (entityId), el tipo de entidad como string ("VARIABLE", "TAG"…) y el texto. El servicio resuelve el tipo contra la tabla module_description_entities, con un caso especial: si el recurso es una variable y además existe en module_description_aggregated_variable, reemplaza el tipo por AGGREGATE. Tras resolver el tipo, obtiene el usuario autenticado vía AccountService, persiste la descripción en DescriptionRepository y publica el evento DescriptionCreated.

Para consultas de lectura, getAllNamedDescriptions combina la tabla central con las tablas mirror: por cada descripción resuelve el nombre legible del recurso consultando el repositorio mirror que corresponda al tipo (getEntityName), devolviendo un NamedDescription con descripción, nombre, tipo, usuario y timestamp listos para mostrar.

Sincronización mediante Event Listeners

Como podemos observar el módulo de descripciones almacena exactamente eso, única y llanamente, descripciones. Para ello fue creado este mismo módulo, para almacenar este dato de forma sencilla y síncrona.

Sin embargo también hemos analizado que este módulo para su propio uso también necesita el nombre de los recursos en si, los cuales se almacenan en sus respectivas tablas. Pero entonces, si hemos dicho que este modulo NO DEBE utilizar información de otros módulos de forma directa ¿como podemos obtener este nombre?

Para este objetivo vamos a usar un término que se va a repetir constantemente durante el desarrollo de este proyecto: eventos de dominio.

Un evento de dominio es una notificación inmutable que representa algo que ha ocurrido en el sistema. En lugar de que el módulo de descripciones consulte activamente los datos de otro módulo, es el propio módulo de origen quien anuncia los cambios que le conciernen: "se ha creado una query", "se ha renombrado una variable", "se ha eliminado un tag". El módulo de descripciones simplemente escucha esos anuncios y reacciona en consecuencia, sin necesidad de conocer los detalles internos del módulo emisor y de una forma asíncrona.

En este caso la implementación usa eventos Spring (ApplicationEvent) con escuchadores anotados con @EventListener. Cuando ocurre un cambio en un recurso, el módulo correspondiente publica el evento y el módulo de descripciones lo captura para mantener su tabla mirror actualizada.

El lado emisor construye el evento con los datos relevantes y lo publica a través del EventBus:

// QueryService.java — módulo de queries
DomainEvent createdEvent = new QueryCreated(dto.getId(), dto.getName());
eventBus.publish(createdEvent);

El lado receptor, en el módulo de descripciones, declara un listener que reacciona a ese evento concreto:

// OnQueryCreatedCreateDescriptionQuery.java — módulo de descripciones
@Service
@DomainEventSubscriber(QueryCreated.class)
public class OnQueryCreatedCreateDescriptionQuery {

    @EventListener
    public void onCreated(final QueryCreated event) {
        creator.create(Long.parseLong(event.aggregateId()), event.getName());
    }
}

El ciclo de vida completo de cada recurso queda cubierto:

EventoListenerEfecto en el módulo de descripciones
QueryCreatedOnQueryCreatedCreateDescriptionQueryCrea registro en module_description_query + descripción vacía
QueryChangedOnQueryChangedChangeDescriptionQueryActualiza el nombre en el mirror
QueryRemovedOnQueryRemovedRemoveDescriptionQueryElimina el mirror y la descripción

El mismo patrón se replica para cada tipo de recurso: AnalogicalVariable, AggregateVariable, Tag, Rule, User, Group y File, con sus respectivos listeners OnXxxCreated, OnXxxChanged y OnXxxRemoved. En todos los casos, la responsabilidad de publicar el evento recae en el servicio del módulo de origen: el módulo de descripciones nunca inicia la comunicación, solo reacciona.