eva4j 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/AGENTS.md +220 -5
  2. package/DOMAIN_YAML_GUIDE.md +188 -3
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/QUICK_REFERENCE.md +8 -4
  5. package/bin/eva4j.js +70 -2
  6. package/config/defaults.json +1 -0
  7. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  8. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  9. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  10. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  11. package/docs/commands/EVALUATE_SYSTEM.md +290 -10
  12. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  13. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  15. package/docs/commands/INDEX.md +27 -3
  16. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  17. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  18. package/docs/prototype/system/RISKS.md +277 -0
  19. package/docs/prototype/system/customers.yaml +133 -0
  20. package/docs/prototype/system/inventory.yaml +109 -0
  21. package/docs/prototype/system/notifications.yaml +131 -0
  22. package/docs/prototype/system/orders.yaml +241 -0
  23. package/docs/prototype/system/payments.yaml +256 -0
  24. package/docs/prototype/system/products.yaml +168 -0
  25. package/docs/prototype/system/system.yaml +269 -0
  26. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  27. package/examples/domain-events.yaml +26 -0
  28. package/examples/domain-read-models.yaml +113 -0
  29. package/examples/system/customer.yaml +89 -0
  30. package/examples/system/orders.yaml +119 -0
  31. package/examples/system/product.yaml +27 -0
  32. package/examples/system/system.yaml +80 -0
  33. package/package.json +1 -1
  34. package/read-model-spec.md +664 -0
  35. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  36. package/src/agents/design-gap-analyst.agent.md +383 -0
  37. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  38. package/src/agents/design-reviewer.agent.md +34 -5
  39. package/src/agents/implement-use-cases.prompt.md +179 -0
  40. package/src/agents/ux-gap-analyst.agent.md +412 -0
  41. package/src/commands/add-rabbitmq-client.js +261 -0
  42. package/src/commands/add-temporal-client.js +22 -2
  43. package/src/commands/build.js +267 -11
  44. package/src/commands/evaluate-system.js +700 -13
  45. package/src/commands/generate-entities.js +560 -24
  46. package/src/commands/generate-http-exchange.js +3 -0
  47. package/src/commands/generate-kafka-event.js +3 -0
  48. package/src/commands/generate-kafka-listener.js +3 -0
  49. package/src/commands/generate-rabbitmq-event.js +665 -0
  50. package/src/commands/generate-rabbitmq-listener.js +205 -0
  51. package/src/commands/generate-record.js +2 -2
  52. package/src/commands/generate-resource.js +4 -1
  53. package/src/commands/generate-temporal-activity.js +970 -33
  54. package/src/commands/generate-temporal-flow.js +98 -38
  55. package/src/commands/generate-temporal-system.js +708 -0
  56. package/src/commands/generate-usecase.js +4 -1
  57. package/src/skills/build-system-yaml/SKILL.md +343 -2
  58. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
  59. package/src/skills/build-system-yaml/references/module-spec.md +90 -9
  60. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  61. package/src/skills/build-temporal-system/SKILL.md +752 -0
  62. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  63. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  64. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  65. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  66. package/src/skills/implement-use-case/SKILL.md +350 -0
  67. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  68. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  69. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  70. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  71. package/src/utils/bounded-context-diagram.js +844 -0
  72. package/src/utils/config-manager.js +4 -2
  73. package/src/utils/domain-validator.js +495 -17
  74. package/src/utils/naming.js +20 -0
  75. package/src/utils/system-validator.js +169 -11
  76. package/src/utils/system-yaml-parser.js +318 -0
  77. package/src/utils/temporal-validator.js +497 -0
  78. package/src/utils/validator.js +3 -1
  79. package/src/utils/yaml-to-entity.js +281 -9
  80. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  81. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  82. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  83. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  84. package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
  85. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  86. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  87. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  88. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  89. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  90. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  91. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  92. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  93. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  94. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  95. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  96. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  97. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  98. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  99. package/templates/base/root/AGENTS.md.ejs +1 -1
  100. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  101. package/templates/crud/EndpointsController.java.ejs +1 -1
  102. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  103. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  104. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  105. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  106. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  107. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  108. package/templates/evaluate/report.html.ejs +1447 -90
  109. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  110. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  111. package/templates/ports/PortAclMapper.java.ejs +35 -0
  112. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  113. package/templates/ports/PortFeignClient.java.ejs +4 -0
  114. package/templates/ports/PortResponseDto.java.ejs +1 -1
  115. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  116. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  117. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  118. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  119. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  120. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  121. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  122. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  123. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  124. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  125. package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
  126. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  127. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  128. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  129. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
  130. package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
  131. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  132. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  133. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  134. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  135. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  136. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  137. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  138. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  139. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  140. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  141. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  142. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  143. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  144. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  145. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  146. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  147. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  148. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  149. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  150. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  151. package/COMMAND_EVALUATION.md +0 -911
package/AGENTS.md CHANGED
@@ -616,12 +616,16 @@ public void cancel() {
616
616
  }
617
617
  ```
618
618
 
619
- Si un evento **no declara `triggers`**, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
619
+ Si un evento **no declara `triggers`** ni `lifecycle`, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
620
620
 
621
621
  **Validaciones generadas:**
622
- - **C2-004** (error): trigger referencia un método que no existe en ninguna transición del módulo
622
+ - **C2-004** (error): trigger referencia un método que no existe en ninguna transición del módulo (se omite para eventos con `lifecycle`)
623
623
  - **C2-005** (info): transición sin ningún evento asociado — considera declarar `triggers`
624
624
  - **C2-001** se silencia automáticamente para transiciones que ya tienen `triggers`
625
+ - **C2-008** (error): valor de `lifecycle` inválido (no es `create`, `update`, `delete` ni `softDelete`)
626
+ - **C2-009** (warning): `lifecycle: softDelete` sin `hasSoftDelete: true` en la entidad raíz, o `lifecycle: delete` con `hasSoftDelete: true`
627
+ - **C2-010** (error): campo de lifecycle event no existe en la entidad raíz del agregado (excluyendo `{entityName}Id` y campos `*At` + `LocalDateTime`)
628
+ - **C2-012** (error): nombre del agregado no coincide con la entidad raíz — el generador usa `aggregate.name` para imports/mappers pero la clase de dominio se genera desde `entity.name`, causando `cannot find symbol`
625
629
 
626
630
  **Auto-wiring de broker:** Si el proyecto tiene un broker de mensajería instalado (`eva add kafka-client`), `eva g entities` genera automáticamente la capa de Integration Events para **todos** los eventos declarados — sin necesidad de ejecutar `eva g kafka-event` por separado:
627
631
 
@@ -658,6 +662,114 @@ public void confirm() {
658
662
 
659
663
  **Nota:** el flag `kafka: true` por evento ya no es necesario — todos los eventos se cablearán automáticamente cuando haya un broker instalado.
660
664
 
665
+ #### Propiedad `lifecycle`
666
+
667
+ Conecta un evento a una operación CRUD del ciclo de vida del agregado. A diferencia de `triggers` (que se conecta a métodos de transición de estado), `lifecycle` emite `raise()` automáticamente en el punto CRUD correspondiente.
668
+
669
+ **Valores válidos:**
670
+
671
+ | Valor | Punto de emisión | Descripción |
672
+ |---|---|---|
673
+ | `create` | Constructor de creación de la entidad | UUID auto-generado como id antes de raise() |
674
+ | `update` | Método `update()` de la entidad raíz | Handler llama `existing.update(...)`; `raise()` interno |
675
+ | `delete` | DeleteCommandHandler, antes de `repository.delete()` | Requiere `hasSoftDelete: false`; genera `repository.delete(entity)` |
676
+ | `softDelete` | Método `softDelete()` de la entidad | Requiere `hasSoftDelete: true`; raise() después de `this.deletedAt = ...` |
677
+
678
+ **Ejemplo en domain.yaml:**
679
+
680
+ ```yaml
681
+ events:
682
+ - name: ProductCreatedEvent
683
+ lifecycle: create
684
+ fields:
685
+ - name: productId
686
+ type: String
687
+ - name: name
688
+ type: String
689
+ - name: price
690
+ type: BigDecimal
691
+
692
+ - name: ProductUpdatedEvent
693
+ lifecycle: update
694
+ fields:
695
+ - name: productId
696
+ type: String
697
+ - name: name
698
+ type: String
699
+
700
+ - name: ProductDeletedEvent
701
+ lifecycle: delete
702
+ fields:
703
+ - name: productId
704
+ type: String
705
+ - name: deletedAt
706
+ type: LocalDateTime
707
+ ```
708
+
709
+ **Resultado generado para `lifecycle: create`:**
710
+
711
+ ```java
712
+ // En Product.java — constructor de creación
713
+ public Product(String name, String description, BigDecimal price) {
714
+ this.id = java.util.UUID.randomUUID().toString();
715
+ this.name = name;
716
+ this.description = description;
717
+ this.price = price;
718
+ raise(new ProductCreatedEvent(this.getId(), this.getName(), this.getPrice()));
719
+ }
720
+ ```
721
+
722
+ **Resultado generado para `lifecycle: update`:**
723
+
724
+ ```java
725
+ // En Product.java — método update()
726
+ public void update(String name, String description, BigDecimal price) {
727
+ this.name = name;
728
+ this.description = description;
729
+ this.price = price;
730
+ raise(new ProductUpdatedEvent(this.getId(), this.getName(), this.getPrice()));
731
+ }
732
+ ```
733
+
734
+ ```java
735
+ // En UpdateProductCommandHandler.java
736
+ Product existing = repository.findById(command.id())...;
737
+ existing.update(
738
+ command.name() != null ? command.name() : existing.getName(),
739
+ command.description() != null ? command.description() : existing.getDescription(),
740
+ command.price() != null ? command.price() : existing.getPrice()
741
+ );
742
+ repository.save(existing);
743
+ ```
744
+
745
+ **Resultado generado para `lifecycle: delete`:**
746
+
747
+ ```java
748
+ // En DeleteProductCommandHandler.java
749
+ Product entity = repository.findById(command.id())...;
750
+ entity.raise(new ProductDeletedEvent(entity.getId(), LocalDateTime.now()));
751
+ repository.delete(entity);
752
+ ```
753
+
754
+ **Resultado generado para `lifecycle: softDelete`:**
755
+
756
+ ```java
757
+ // En Product.java — método softDelete()
758
+ public void softDelete() {
759
+ if (this.deletedAt != null) { throw new IllegalStateException("..."); }
760
+ this.deletedAt = java.time.LocalDateTime.now();
761
+ raise(new ProductDeactivatedEvent(this.getId(), LocalDateTime.now()));
762
+ }
763
+ ```
764
+
765
+ **Resolución de argumentos:** usa las mismas reglas que `triggers` (match por nombre de campo, VO unwrapping, `LocalDateTime.now()` para campos *At, `null /* TODO */` para no resueltos).
766
+
767
+ **Visibilidad de `raise()`:** cuando un evento usa `lifecycle: delete`, el método `raise()` en la entidad se genera como `public` (en vez de `protected`) para permitir que los handlers de aplicación lo invoquen. Con `lifecycle: update`, `raise()` permanece `protected` porque se invoca internamente desde el método `update()` de la entidad.
768
+
769
+ **Infraestructura de repositorio:** cuando un evento usa `lifecycle: delete`, el generador agrega automáticamente `void delete(Entity entity)` al repositorio (interface + implementación). La implementación publica los eventos pendientes antes de la eliminación física.
770
+
771
+ **Restricción:** un evento puede declarar `triggers` O `lifecycle`, no ambos. `lifecycle` aplica solo a eventos de la entidad raíz del agregado.
772
+
661
773
  ### Consumo de Eventos Externos (`listeners[]`)
662
774
 
663
775
  ```yaml
@@ -821,8 +933,94 @@ aggregates:
821
933
  └── events: → Domain Events que PRODUCE (async, broker)
822
934
  listeners: → Integration Events que CONSUME (async, broker)
823
935
  ports: → Servicios HTTP que LLAMA (sync, Feign)
936
+ readModels: → Proyecciones locales mantenidas por eventos (async, broker)
824
937
  ```
825
938
 
939
+ ### Proyecciones Locales (`readModels[]`)
940
+
941
+ Un Read Model es una **proyección local de datos de otro bounded context**, mantenida mediante eventos de dominio. Elimina dependencias síncronas (HTTP) entre módulos, mejorando autonomía, resiliencia y rendimiento.
942
+
943
+ ```yaml
944
+ # Nivel raíz, sibling de aggregates:, listeners:, ports:
945
+ readModels:
946
+ - name: ProductReadModel # PascalCase + sufijo "ReadModel" (OBLIGATORIO)
947
+ source: # Trazabilidad al módulo fuente (OBLIGATORIO)
948
+ module: products # Módulo fuente (kebab-case)
949
+ aggregate: Product # Agregado fuente (PascalCase)
950
+ tableName: rm_products # Tabla en BD (OBLIGATORIO, prefijo rm_)
951
+ fields: # Campos proyectados — subconjunto del fuente
952
+ - name: id
953
+ type: String
954
+ - name: name
955
+ type: String
956
+ - name: price
957
+ type: BigDecimal
958
+ - name: status
959
+ type: String
960
+ syncedBy: # Eventos que mantienen esta tabla (min 1)
961
+ - event: ProductCreatedEvent # Nombre del evento (PascalCase + sufijo Event)
962
+ action: UPSERT # Acción: UPSERT | DELETE | SOFT_DELETE
963
+ - event: ProductUpdatedEvent
964
+ action: UPSERT
965
+ - event: ProductDeactivatedEvent
966
+ action: SOFT_DELETE
967
+ ```
968
+
969
+ ### Artefactos generados por `readModels[]`
970
+
971
+ Por cada read model:
972
+
973
+ | Archivo generado | Descripción |
974
+ |---|---|
975
+ | `domain/models/readmodels/{Name}.java` | Clase de dominio (inmutable, sin setters, sin auditoría) |
976
+ | `infrastructure/database/entities/{Name}Jpa.java` | Entidad JPA (Lombok, `@Id` NO auto-generado) |
977
+ | `infrastructure/database/repositories/{Name}JpaRepository.java` | Spring Data JPA interface |
978
+ | `domain/repositories/{Name}Repository.java` | Interfaz de repositorio (puerto) |
979
+ | `infrastructure/database/repositories/{Name}RepositoryImpl.java` | Implementación del repositorio |
980
+ | `application/usecases/Sync{Source}ReadModelHandler.java` | Handler de sincronización (un método por evento) |
981
+
982
+ Por cada entrada en `syncedBy`:
983
+
984
+ | Archivo generado | Descripción |
985
+ |---|---|
986
+ | `application/events/{EventBase}IntegrationEvent.java` | Integration Event (reutilizado si ya existe) |
987
+ | `infrastructure/kafkaListener/{EventBase}ReadModelListener.java` | Kafka listener que delega al sync handler |
988
+ | `parameters/*/kafka.yaml` | Registro del topic (actualizado) |
989
+
990
+ ### Acciones de sincronización
991
+
992
+ | Acción | Significado | Uso |
993
+ |---|---|---|
994
+ | `UPSERT` | Insertar si es nuevo, actualizar si existe | Creaciones, actualizaciones, cambios de estado |
995
+ | `DELETE` | Eliminar el registro permanentemente | Hard deletes en el módulo fuente |
996
+ | `SOFT_DELETE` | Marcar como inactivo con timestamp | Cuando el fuente usa soft delete |
997
+
998
+ > **Nota:** Para acciones `DELETE` y `SOFT_DELETE`, el listener **no usa IntegrationEvent** — extrae directamente el `id` del payload y lo pasa como `String` al `SyncHandler`. El `SyncHandler` llama a `repository.deleteById(id)` o `repository.softDeleteById(id)` respectivamente. El generador tampoco produce el archivo IntegrationEvent para estas acciones.
999
+
1000
+ ### Reglas de `readModels[]`
1001
+
1002
+ - **`name:`** — PascalCase, **DEBE** terminar con `ReadModel`
1003
+ - **`tableName:`** — **DEBE** empezar con `rm_` (identificación visual en BD)
1004
+ - **`fields:`** — **DEBE** incluir un campo `id`
1005
+ - **`syncedBy:`** — **DEBE** tener al menos una entrada
1006
+ - **`source.module:`** — **NO PUEDE** ser el mismo módulo actual (readModels son para proyecciones cross-module)
1007
+ - **Topic derivado automáticamente** — Se deriva del nombre del evento: strip sufijo `Event` → SCREAMING_SNAKE_CASE. Override opcional con `topic:` explícito
1008
+ - **Sin auditoría** — Los readModels **nunca** tienen campos de auditoría
1009
+ - **Sin endpoints REST** — Los readModels **nunca** exponen endpoints REST
1010
+ - **Sin lógica de negocio** — La clase de dominio es inmutable (solo getters)
1011
+
1012
+ ### Validaciones
1013
+
1014
+ | Código | Severidad | Regla |
1015
+ |---|---|---|
1016
+ | RM-001 | ERROR | `name` debe terminar con `ReadModel` |
1017
+ | RM-002 | ERROR | `tableName` debe empezar con `rm_` |
1018
+ | RM-004 | ERROR | `fields` debe incluir un campo `id` |
1019
+ | RM-005 | ERROR | `syncedBy` debe tener al menos una entrada |
1020
+ | RM-006 | ERROR | `syncedBy[].action` debe ser `UPSERT`, `DELETE` o `SOFT_DELETE` |
1021
+ | RM-009 | WARNING | `ports:` todavía tiene llamadas sync al mismo `source.module` — considerar removerlas |
1022
+ | RM-010 | ERROR | `source.module` es el mismo módulo actual |
1023
+
826
1024
  Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
827
1025
 
828
1026
  ### Configuración en domain.yaml
@@ -1246,6 +1444,7 @@ private String customerId;
1246
1444
  5. **SABER** que cualquier otro nombre genera un **scaffold** con `UnsupportedOperationException` — el desarrollador debe implementar el handler
1247
1445
  6. **APLICAR** la regla anti-duplicado: si el mismo useCase aparece en v1 y v2, se genera solo una vez
1248
1446
  7. **NOMBRAR** los controladores según la convención: `{Aggregate}{VersionCapitalized}Controller` (ej: `OrderV1Controller`)
1447
+ 8. **SI** el módulo tiene **2+ agregados** → usar `basePath: ""` (string vacío, **NO** `basePath: /`) y paths absolutos por operación (ej: `/products`, `/categories/{id}`). Si tiene un solo agregado → `basePath: /recurso` con paths relativos
1249
1448
 
1250
1449
  ### Al Generar Código de Dominio
1251
1450
 
@@ -1478,7 +1677,13 @@ Al generar o modificar código, verificar:
1478
1677
  - [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
1479
1678
  - [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
1480
1679
  - [ ] Evento con `triggers: [methodName]` → el generador emite `raise()` automáticamente; args no resolubles quedan como `null /* TODO */`
1481
- - [ ] Sin `triggers` en el evento el dev llama a `raise()` manualmente
1680
+ - [ ] Evento con `lifecycle: create` el generador emite UUID auto-generado + `raise()` en el constructor de creación
1681
+ - [ ] Evento con `lifecycle: update` → el generador emite método `update()` en la entidad raíz con `raise()` interno; handler llama `existing.update(...)`; `raise()` permanece `protected`
1682
+ - [ ] Evento con `lifecycle: delete` → el generador emite `raise()` en DeleteCommandHandler + genera `repository.delete(entity)` con publicación de eventos; `raise()` se genera como `public`
1683
+ - [ ] Evento con `lifecycle: softDelete` → el generador emite `raise()` dentro del método `softDelete()` de la entidad; requiere `hasSoftDelete: true`
1684
+ - [ ] Un evento puede declarar `triggers` O `lifecycle`, no ambos
1685
+ - [ ] Campos de lifecycle events son campos de la entidad raíz (excluyendo `{entityName}Id` y `*At` temporal) — `C2-010`
1686
+ - [ ] Sin `triggers` ni `lifecycle` en el evento → el dev llama a `raise()` manualmente
1482
1687
  - [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
1483
1688
  - [ ] Distinguir entre Domain Event (`domain/models/events/X.java`) e Integration Event (`application/events/XIntegrationEvent.java`) — cambios de broker solo afectan al adaptador `MessageBroker`
1484
1689
  - [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
@@ -1493,9 +1698,19 @@ Al generar o modificar código, verificar:
1493
1698
  - [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
1494
1699
  - [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si es necesario
1495
1700
  - [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio), FeignClient (devuelve DTOs infra), FeignAdapter (mapea ACL), FeignConfig + `urls.yaml`
1701
+ - [ ] Read models → declarar en `readModels[]` (nivel raíz); `name` debe terminar con `ReadModel`, `tableName` debe empezar con `rm_`
1702
+ - [ ] Read models nunca tienen auditoría, endpoints REST, ni lógica de negocio
1703
+ - [ ] Cada read model genera: clase de dominio inmutable, JPA entity (sin audit), repositorio (interface + impl), sync handler
1704
+ - [ ] Cada `syncedBy` entry genera: IntegrationEvent (reutilizado si ya existe), KafkaListener, registro de topic
1705
+ - [ ] Para `DELETE`/`SOFT_DELETE`, el listener bypasses IntegrationEvent — extrae `id` directamente y pasa `String id` al SyncHandler
1706
+ - [ ] `SOFT_DELETE` → `repository.softDeleteById(id)`; `DELETE` → `repository.deleteById(id)`; `UPSERT` → reconstruye modelo completo con IntegrationEvent + `repository.upsert()`
1707
+ - [ ] `source.module` nunca puede ser el mismo módulo (RM-010) — readModels son exclusivamente cross-module
1708
+ - [ ] ReadModel fields cubiertos por eventos UPSERT del productor — `C1-007`
1709
+ - [ ] ReadModel fields son subconjunto de los campos de la entidad raíz fuente (por C2-010, los lifecycle events no pueden emitir campos ajenos)
1710
+ - [ ] Topics de readModels se derivan automáticamente del nombre del evento (strip `Event` → SCREAMING_SNAKE_CASE)
1496
1711
 
1497
1712
  ---
1498
1713
 
1499
- **Última actualización:** 2026-03-12
1500
- **Versión de eva4j:** 1.0.14
1714
+ **Última actualización:** 2026-03-24
1715
+ **Versión de eva4j:** 1.0.15
1501
1716
  **Estado:** Documento de referencia para agentes IA
@@ -139,6 +139,9 @@ aggregates:
139
139
  # Eventos de dominio que emite este agregado (dentro del agregado)
140
140
  - name: NombreEventoOcurrido
141
141
  fields: []
142
+ # Opciones mutuamente excluyentes para emisión automática:
143
+ # triggers: [methodName] # Emite raise() dentro de un método de transición
144
+ # lifecycle: create # Emite raise() en operación CRUD (create|update|delete|softDelete)
142
145
 
143
146
  # listeners: — eventos externos que CONSUME este módulo (nivel raíz)
144
147
  listeners:
@@ -147,6 +150,18 @@ listeners:
147
150
  topic: TOPIC_NAME # Topic Kafka (obligatorio en módulos standalone)
148
151
  useCase: HandleExternalEvent # Caso de uso que maneja el evento
149
152
  fields: [] # Payload del Integration Event recibido
153
+
154
+ # readModels: — proyecciones locales de datos de otro módulo (nivel raíz)
155
+ readModels:
156
+ - name: ProductReadModel # PascalCase + sufijo ReadModel
157
+ source:
158
+ module: products # Módulo fuente
159
+ aggregate: Product # Agregado fuente
160
+ tableName: rm_products # Tabla SQL (prefijo rm_)
161
+ fields: [] # Campos proyectados
162
+ syncedBy: # Eventos que sincronizan la tabla
163
+ - event: ProductCreatedEvent
164
+ action: UPSERT # UPSERT | DELETE | SOFT_DELETE
150
165
  ```
151
166
 
152
167
  ### Ubicación del archivo
@@ -1711,13 +1726,62 @@ aggregates:
1711
1726
  |-----------|------|-------------|-------------|
1712
1727
  | `name` | String | ✅ | Nombre de la clase del evento (PascalCase) |
1713
1728
  | `fields` | Array | ✅ | Campos que transporta el evento |
1714
- | `triggers` | Array\<String\> | ➖ | Nombres de métodos de transición que publican este evento. El generador emite `raise(new XEvent(...))` automáticamente. |
1729
+ | `triggers` | Array\<String\> | ➖ | Nombres de métodos de transición que publican este evento. El generador emite `raise(new XEvent(...))` automáticamente. Mutuamente excluyente con `lifecycle`. |
1730
+ | `lifecycle` | String | ➖ | Operación CRUD que publica este evento: `create`, `update`, `delete`, `softDelete`. Mutuamente excluyente con `triggers`. |
1715
1731
  | `topic` | String | ➖ | **Override del topic Kafka.** Por defecto el generador quita el sufijo `Event` del nombre de la clase: `ProductPublishedEvent` → `PRODUCT_PUBLISHED`. Úsalo cuando el consumer en `listeners[]` de otro módulo declara un topic diferente al auto-derivado. |
1716
1732
 
1717
1733
  > **Regla de derivación de topic:** `ProductPublishedEvent` → quita `Event` → `ProductPublished` → SCREAMING_SNAKE → `PRODUCT_PUBLISHED`. Esto garantiza que el producer y el consumer coincidan cuando el consumer declara `topic: PRODUCT_PUBLISHED` en `listeners[]`.
1718
1734
 
1719
1735
  > **`kafka: true`** ya no es necesario — si el proyecto tiene `kafka-client` instalado, todos los eventos se cablearán automáticamente al ejecutar `eva g entities`.
1720
1736
 
1737
+ ### Ejemplo con `lifecycle:`
1738
+
1739
+ ```yaml
1740
+ events:
1741
+ - name: ProductCreatedEvent
1742
+ lifecycle: create # raise() en el constructor de creación
1743
+ fields:
1744
+ - name: productId
1745
+ type: String
1746
+ - name: name
1747
+ type: String
1748
+
1749
+ - name: ProductUpdatedEvent
1750
+ lifecycle: update # raise() en UpdateCommandHandler antes de save()
1751
+ fields:
1752
+ - name: productId
1753
+ type: String
1754
+ - name: name
1755
+ type: String
1756
+
1757
+ - name: ProductDeletedEvent
1758
+ lifecycle: delete # raise() en DeleteCommandHandler; requiere !hasSoftDelete
1759
+ fields:
1760
+ - name: productId
1761
+ type: String
1762
+ - name: deletedAt
1763
+ type: LocalDateTime
1764
+
1765
+ - name: ProductDeactivatedEvent
1766
+ lifecycle: softDelete # raise() en softDelete(); requiere hasSoftDelete: true
1767
+ fields:
1768
+ - name: productId
1769
+ type: String
1770
+ - name: deletedAt
1771
+ type: LocalDateTime
1772
+ ```
1773
+
1774
+ **Puntos de emisión:**
1775
+
1776
+ | `lifecycle` | Punto de emisión | UUID auto-gen | `raise()` visibility |
1777
+ |---|---|---|---|
1778
+ | `create` | Constructor de creación | ✅ `this.id = UUID.randomUUID().toString()` | `protected` |
1779
+ | `update` | `UpdateCommandHandler`, antes de `repository.save()` | — | `public` |
1780
+ | `delete` | `DeleteCommandHandler`, antes de `repository.delete()` | — | `public` |
1781
+ | `softDelete` | Método `softDelete()` de la entidad | — | `protected` |
1782
+
1783
+ > **Validaciones:** `C2-008` (error) si el valor de lifecycle no es válido. `C2-009` (warning) si `lifecycle: softDelete` sin `hasSoftDelete: true`, o `lifecycle: delete` con `hasSoftDelete: true`. `C2-010` (error) si un campo del lifecycle event no existe en la entidad raíz del agregado.
1784
+
1721
1785
  ### Archivos generados
1722
1786
 
1723
1787
  Para cada evento, eva4j genera dos archivos:
@@ -1946,7 +2010,9 @@ domain.yaml
1946
2010
 
1947
2011
  ├── listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
1948
2012
 
1949
- └── ports: → Servicios HTTP que LLAMA (infrastructure/adapters/{service}/)
2013
+ ├── ports: → Servicios HTTP que LLAMA (infrastructure/adapters/{service}/)
2014
+
2015
+ └── readModels: → Proyecciones locales mantenidas por eventos (infrastructure/kafkaListener/)
1950
2016
  ```
1951
2017
 
1952
2018
  ---
@@ -2082,6 +2148,123 @@ Ver ejemplo completo en [`examples/domain-ports.yaml`](examples/domain-ports.yam
2082
2148
 
2083
2149
  ---
2084
2150
 
2151
+ ## Sección readModels
2152
+
2153
+ `readModels:` es una sección de nivel raíz (sibling de `aggregates:`, `listeners:` y `ports:`) que declara **proyecciones locales de datos de otro bounded context**, mantenidas mediante eventos de dominio. Elimina dependencias síncronas (HTTP) entre módulos.
2154
+
2155
+ ### Estructura completa
2156
+
2157
+ ```yaml
2158
+ readModels:
2159
+ - name: ProductReadModel # PascalCase + sufijo "ReadModel" (OBLIGATORIO)
2160
+ source: # Trazabilidad al módulo fuente (OBLIGATORIO)
2161
+ module: products # Módulo fuente (kebab-case)
2162
+ aggregate: Product # Agregado fuente (PascalCase)
2163
+ tableName: rm_products # Tabla en BD (OBLIGATORIO, prefijo rm_)
2164
+ fields: # Campos proyectados (OBLIGATORIO, debe incluir id)
2165
+ - name: id
2166
+ type: String
2167
+ - name: name
2168
+ type: String
2169
+ - name: price
2170
+ type: BigDecimal
2171
+ - name: status
2172
+ type: String
2173
+ syncedBy: # Eventos que mantienen la tabla (OBLIGATORIO, min 1)
2174
+ - event: ProductCreatedEvent # Nombre del evento (PascalCase)
2175
+ action: UPSERT # UPSERT | DELETE | SOFT_DELETE
2176
+ - event: ProductUpdatedEvent
2177
+ action: UPSERT
2178
+ - event: ProductDeactivatedEvent
2179
+ action: SOFT_DELETE
2180
+ ```
2181
+
2182
+ ### Propiedades
2183
+
2184
+ | Propiedad | Tipo | Obligatorio | Descripción |
2185
+ |---|---|---|---|
2186
+ | `name` | String | SÍ | Nombre del read model. **DEBE** terminar con `ReadModel`. PascalCase. |
2187
+ | `source` | Object | SÍ | Trazabilidad al módulo y agregado fuente. |
2188
+ | `source.module` | String | SÍ | Módulo que posee los datos. Kebab-case. |
2189
+ | `source.aggregate` | String | SÍ | Agregado fuente. PascalCase. |
2190
+ | `tableName` | String | SÍ | Nombre de tabla SQL. **DEBE** empezar con `rm_`. |
2191
+ | `fields` | Array | SÍ | Campos a proyectar. Debe incluir `id`. |
2192
+ | `syncedBy` | Array | SÍ | Eventos que sincronizan esta tabla. Mínimo 1. |
2193
+ | `syncedBy[].event` | String | SÍ | Nombre del evento. PascalCase. |
2194
+ | `syncedBy[].action` | String | SÍ | Uno de: `UPSERT`, `DELETE`, `SOFT_DELETE`. |
2195
+ | `syncedBy[].topic` | String | NO | Override del topic Kafka. Por defecto se auto-deriva del nombre del evento. |
2196
+
2197
+ ### Acciones de sincronización
2198
+
2199
+ | Acción | Significado | Cuándo usarla |
2200
+ |---|---|---|
2201
+ | `UPSERT` | Insertar si no existe, actualizar si existe | Creaciones, actualizaciones, cambios de estado |
2202
+ | `DELETE` | Eliminar permanentemente | Hard deletes en el módulo fuente |
2203
+ | `SOFT_DELETE` | Marcar como inactivo con timestamp | Cuando el fuente usa soft delete |
2204
+
2205
+ ### Derivación automática de topics
2206
+
2207
+ El topic se deriva del nombre del evento: se elimina el sufijo `Event` y se convierte a SCREAMING_SNAKE_CASE:
2208
+
2209
+ - `ProductCreatedEvent` → `PRODUCT_CREATED`
2210
+ - `CustomerUpdatedEvent` → `CUSTOMER_UPDATED`
2211
+ - `OrderCancelled` → `ORDER_CANCELLED`
2212
+
2213
+ Para sobrescribir, usar `topic:` explícito en la entrada de `syncedBy`.
2214
+
2215
+ ### Artefactos generados
2216
+
2217
+ **Por cada read model (6 archivos):**
2218
+
2219
+ | Archivo | Descripción |
2220
+ |---|---|
2221
+ | `domain/models/readmodels/{Name}.java` | Clase de dominio inmutable (sin setters, sin auditoría) |
2222
+ | `infrastructure/database/entities/{Name}Jpa.java` | Entidad JPA con Lombok. `@Id` no auto-generado |
2223
+ | `infrastructure/database/repositories/{Name}JpaRepository.java` | Spring Data JPA interface |
2224
+ | `domain/repositories/{Name}Repository.java` | Interfaz de repositorio (puerto) |
2225
+ | `infrastructure/database/repositories/{Name}RepositoryImpl.java` | Implementación del repositorio |
2226
+ | `application/usecases/Sync{Source}ReadModelHandler.java` | Handler con un método por evento |
2227
+
2228
+ **Por cada entrada en `syncedBy` (hasta 3 archivos):**
2229
+
2230
+ | Archivo | Descripción |
2231
+ |---|---|
2232
+ | `application/events/{EventBase}IntegrationEvent.java` | Integration Event (reutilizado si ya existe) |
2233
+ | `infrastructure/kafkaListener/{EventBase}ReadModelListener.java` | Kafka listener que delega al sync handler |
2234
+ | `parameters/*/kafka.yaml` | Registro del topic |
2235
+
2236
+ ### Validaciones
2237
+
2238
+ | Código | Severidad | Regla |
2239
+ |---|---|---|
2240
+ | RM-001 | ERROR | `name` debe terminar con `ReadModel` |
2241
+ | RM-002 | ERROR | `tableName` debe empezar con `rm_` |
2242
+ | RM-004 | ERROR | `fields` debe incluir un campo `id` |
2243
+ | RM-005 | ERROR | `syncedBy` debe tener al menos una entrada |
2244
+ | RM-006 | ERROR | `syncedBy[].action` debe ser `UPSERT`, `DELETE` o `SOFT_DELETE` |
2245
+ | RM-008 / C1-007 | WARNING | Campo del readModel no cubierto por ningún evento UPSERT del productor — siempre será null |
2246
+ | RM-009 | WARNING | `ports:` tiene llamadas sync al mismo `source.module` — considerar eliminarlas |
2247
+ | RM-010 | ERROR | `source.module` es el mismo módulo actual |
2248
+
2249
+ ### Diferencias con agregados
2250
+
2251
+ | Característica | Aggregate | Read Model |
2252
+ |---|---|---|
2253
+ | Sección YAML | `aggregates:` | `readModels:` |
2254
+ | Prefijo de tabla | Ninguno | `rm_` |
2255
+ | Lógica de negocio | Sí | No |
2256
+ | Emite eventos | Sí | Nunca |
2257
+ | Endpoints REST | Opcional | Nunca |
2258
+ | Clase de dominio | Entidad rica | Clase inmutable |
2259
+ | Campos de auditoría | Opcionales | Nunca |
2260
+ | `source:` requerido | No | Sí |
2261
+
2262
+ ### Referencia
2263
+
2264
+ Ver ejemplo completo en [`examples/domain-read-models.yaml`](examples/domain-read-models.yaml).
2265
+
2266
+ ---
2267
+
2085
2268
  ## Relaciones
2086
2269
 
2087
2270
  eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
@@ -2154,7 +2337,7 @@ public void removeOrderItem(OrderItem orderItem) {
2154
2337
 
2155
2338
  **Genera en JPA:**
2156
2339
  ```java
2157
- @OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, fetch = FetchType.LAZY)
2340
+ @OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true, fetch = FetchType.LAZY)
2158
2341
  @Builder.Default
2159
2342
  private List<OrderItemJpa> orderItems = new ArrayList<>();
2160
2343
  ```
@@ -2382,6 +2565,8 @@ relationships:
2382
2565
  | `DETACH` | Al separar el padre, separa los hijos | ⚠️ Rara vez necesario |
2383
2566
  | `ALL` | Todas las operaciones anteriores | ⚠️ Solo si estás seguro |
2384
2567
 
2568
+ > **Nota:** eva4j genera automáticamente `orphanRemoval = true` en todas las relaciones `@OneToMany` y `@OneToOne(mappedBy)`. Esto asegura que al remover un hijo de la colección del padre, JPA elimina la fila de la base de datos. Es el comportamiento correcto en DDD: las entidades secundarias del agregado no tienen existencia independiente — su ciclo de vida lo controla la raíz.
2569
+
2385
2570
  #### **Configuraciones Recomendadas:**
2386
2571
 
2387
2572
  ```yaml
@@ -459,66 +459,47 @@ public ResponseEntity<ErrorDto> handleOptimisticLock(ObjectOptimisticLockingFail
459
459
 
460
460
  ---
461
461
 
462
- ## 6. Read Models Separados (Proyecciones)
462
+ ## 6. Read Models Separados (Proyecciones)
463
+
464
+ > **Implementado en v1.0.15** — Sección `readModels:` en `domain.yaml` con generación automática de proyecciones locales mantenidas por eventos Kafka. Ver [`DOMAIN_YAML_GUIDE.md`](DOMAIN_YAML_GUIDE.md#sección-readmodels) y [`read-model-spec.md`](read-model-spec.md).
463
465
 
464
466
  ### Descripción
465
467
 
466
- En CQRS puro, el lado de lectura puede tener su propio modelo optimizado para consultas, independiente del modelo de escritura. Los `*ResponseDto` actuales son transformaciones directas del dominio, suficiente para casos simples pero insuficientes para reportes o vistas que joinean múltiples agregados.
468
+ Proyecciones locales de datos de otro bounded context, mantenidas mediante eventos de dominio. Elimina dependencias síncronas (HTTP) entre módulos.
467
469
 
468
- ### Sintaxis Propuesta
470
+ ### Sintaxis Implementada
469
471
 
470
472
  ```yaml
471
- aggregates:
472
- - name: Order
473
- readModels:
474
- - name: OrderSummary
475
- description: "Vista desnormalizada para listados"
476
- fields:
477
- - name: id
478
- type: String
479
- - name: orderNumber
480
- type: String
481
- - name: customerName
482
- type: String
483
- - name: totalAmount
484
- type: BigDecimal
485
- - name: itemCount
486
- type: Integer
487
- - name: status
488
- type: OrderStatus
489
- source: native_query
473
+ readModels:
474
+ - name: ProductReadModel
475
+ source:
476
+ module: products
477
+ aggregate: Product
478
+ tableName: rm_products
479
+ fields:
480
+ - name: id
481
+ type: String
482
+ - name: name
483
+ type: String
484
+ - name: price
485
+ type: BigDecimal
486
+ syncedBy:
487
+ - event: ProductCreatedEvent
488
+ action: UPSERT
489
+ - event: ProductUpdatedEvent
490
+ action: UPSERT
491
+ - event: ProductDeactivatedEvent
492
+ action: SOFT_DELETE
490
493
  ```
491
494
 
492
- ### Código Generado
495
+ ### Artefactos Generados
493
496
 
494
- ```java
495
- public interface OrderSummaryProjection {
496
- String getId();
497
- String getOrderNumber();
498
- String getCustomerName();
499
- BigDecimal getTotalAmount();
500
- Integer getItemCount();
501
- OrderStatus getStatus();
502
- }
503
- ```
504
-
505
- ```java
506
- @Query(value = """
507
- SELECT
508
- o.id,
509
- o.order_number AS orderNumber,
510
- c.name AS customerName,
511
- o.total_amount AS totalAmount,
512
- COUNT(i.id) AS itemCount,
513
- o.status
514
- FROM orders o
515
- JOIN customers c ON c.id = o.customer_id
516
- LEFT JOIN order_items i ON i.order_id = o.id
517
- WHERE o.deleted = false
518
- GROUP BY o.id, c.name
519
- """, nativeQuery = true)
520
- Page<OrderSummaryProjection> findOrderSummaries(Pageable pageable);
521
- ```
497
+ - Clase de dominio inmutable (`domain/models/readmodels/`)
498
+ - Entidad JPA sin auditoría (`infrastructure/database/entities/`)
499
+ - Repositorio (interfaz + impl)
500
+ - Sync handler con un método por evento
501
+ - Kafka listeners (uno por entry en `syncedBy`)
502
+ - Integration Events (reutilizados si ya existen)
522
503
 
523
504
  ---
524
505
 
@@ -1607,7 +1588,7 @@ Sin DataFaker, el seeder genera datos con patrones simples (`"User 0"`, `"user0@
1607
1588
  | 3 | Soft Delete Completo | Alta | Baja | ✅ Implementado |
1608
1589
  | 4 | Paginación en Queries | Impl. | -- | ✅ Implementado |
1609
1590
  | 5 | Optimistic Locking | Media | Baja | Pendiente |
1610
- | 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
1591
+ | 6 | Read Models / Proyecciones | Media | Alta | Implementado |
1611
1592
  | 7 | Enums con Transiciones | Impl. | -- | ✅ Implementado |
1612
1593
  | 8 | Specifications Pattern | Media | Media | Pendiente |
1613
1594
  | 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
@@ -120,13 +120,17 @@ eva detach order
120
120
 
121
121
  ## 🧩 Temporal Queue Model
122
122
 
123
+ Queues are **module-scoped** — each module gets its own set of queues prefixed with the module name in SCREAMING_SNAKE_CASE:
124
+
123
125
  | Queue | Purpose | ActivityOptions var |
124
126
  |-------|---------|---------------------|
125
- | `FLOW_QUEUE` | Workflow orchestration | — |
126
- | `LIGHT_TASK_QUEUE` | Fast activities < 30 s | `lightActivityOptions` |
127
- | `HEAVY_TASK_QUEUE` | Long-running ≤ 2 min | `heavyActivityOptions` |
127
+ | `{MODULE}_WORKFLOW_QUEUE` | Workflow orchestration | — |
128
+ | `{MODULE}_LIGHT_TASK_QUEUE` | Fast activities < 30 s | `lightActivityOptions` |
129
+ | `{MODULE}_HEAVY_TASK_QUEUE` | Long-running ≤ 2 min | `heavyActivityOptions` |
130
+
131
+ Example for module `order`: `ORDER_WORKFLOW_QUEUE`, `ORDER_LIGHT_TASK_QUEUE`, `ORDER_HEAVY_TASK_QUEUE`.
128
132
 
129
- Activities are registered automatically via Spring DI (`LightActivity` / `HeavyActivity` marker interfaces). No manual `TemporalConfig.java` patching needed.
133
+ Activities are registered automatically via Spring DI (`{Module}LightActivity` / `{Module}HeavyActivity` marker interfaces). Each module has its own `{Module}TemporalWorkerConfig.java` — no shared `TemporalConfig.java` patching needed.
130
134
 
131
135
  ---
132
136