eva4j 1.0.11 → 1.0.12

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 (58) hide show
  1. package/AGENTS.md +15 -5
  2. package/DOMAIN_YAML_GUIDE.md +51 -0
  3. package/FUTURE_FEATURES.md +222 -115
  4. package/QUICK_REFERENCE.md +101 -153
  5. package/README.md +77 -70
  6. package/bin/eva4j.js +57 -1
  7. package/config/defaults.json +3 -0
  8. package/docs/commands/GENERATE_ENTITIES.md +886 -40
  9. package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
  10. package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
  11. package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
  12. package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
  13. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
  14. package/docs/commands/GENERATE_USECASE.md +216 -282
  15. package/docs/commands/INDEX.md +36 -7
  16. package/examples/domain-events.yaml +201 -0
  17. package/examples/domain-multi-aggregate.yaml +6 -0
  18. package/package.json +2 -2
  19. package/src/commands/add-kafka-client.js +3 -1
  20. package/src/commands/add-temporal-client.js +286 -0
  21. package/src/commands/generate-entities.js +75 -4
  22. package/src/commands/generate-kafka-event.js +273 -89
  23. package/src/commands/generate-temporal-activity.js +228 -0
  24. package/src/commands/generate-temporal-flow.js +216 -0
  25. package/src/generators/module-generator.js +1 -0
  26. package/src/generators/shared-generator.js +26 -0
  27. package/src/utils/yaml-to-entity.js +26 -4
  28. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  29. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
  30. package/templates/aggregate/AggregateRoot.java.ejs +33 -1
  31. package/templates/aggregate/DomainEntity.java.ejs +1 -1
  32. package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
  33. package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
  34. package/templates/aggregate/JpaAggregateRoot.java.ejs +1 -0
  35. package/templates/aggregate/JpaEntity.java.ejs +2 -1
  36. package/templates/base/docker/kafka-services.yaml.ejs +2 -2
  37. package/templates/base/docker/temporal-services.yaml.ejs +29 -0
  38. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
  39. package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
  40. package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
  41. package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
  42. package/templates/crud/Controller.java.ejs +36 -6
  43. package/templates/crud/ListQuery.java.ejs +6 -2
  44. package/templates/crud/ListQueryHandler.java.ejs +24 -10
  45. package/templates/crud/UpdateCommand.java.ejs +52 -0
  46. package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
  47. package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
  48. package/templates/kafka-event/Event.java.ejs +23 -0
  49. package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
  50. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
  51. package/templates/shared/domain/DomainEvent.java.ejs +40 -0
  52. package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
  53. package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
  54. package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
  55. package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
  56. package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
  57. package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
  58. package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
package/AGENTS.md CHANGED
@@ -387,19 +387,28 @@ void assignUser(User user) { // package-private
387
387
 
388
388
  ```bash
389
389
  # Crear proyecto
390
- eva4j create my-app
390
+ eva create my-app
391
391
 
392
392
  # Agregar módulo
393
- eva4j add module users
393
+ eva add module users
394
394
 
395
395
  # Generar entidades desde YAML
396
- eva4j g entities users
396
+ eva g entities users
397
397
 
398
398
  # Generar use case
399
- eva4j g usecase users ActivateUser
399
+ eva g usecase users ActivateUser
400
400
 
401
401
  # Generar resource (REST)
402
- eva4j g resource users
402
+ eva g resource users
403
+
404
+ # Agregar cliente Temporal
405
+ eva add temporal-client
406
+
407
+ # Generar workflow Temporal
408
+ eva g temporal-flow users
409
+
410
+ # Generar actividad Temporal
411
+ eva g temporal-activity users
403
412
  ```
404
413
 
405
414
  ### Estructura de domain.yaml
@@ -558,6 +567,7 @@ Los campos en domain.yaml soportan las siguientes propiedades:
558
567
  | **`readOnly`** | Boolean | `false` | **Excluye del constructor de negocio y CreateDto** |
559
568
  | **`hidden`** | Boolean | `false` | **Excluye del ResponseDto** |
560
569
  | **`validations`** | Array | `[]` | **Anotaciones JSR-303 en el Command y CreateDto** |
570
+ | **`reference`** | Object | `null` | **Declara referencia semántica a otro agregado (genera comentario Javadoc)** |
561
571
 
562
572
  #### Flags de Visibilidad: `readOnly` y `hidden`
563
573
 
@@ -683,6 +683,57 @@ fields:
683
683
 
684
684
  ---
685
685
 
686
+ ### Referencias entre Agregados (`reference:`)
687
+
688
+ La propiedad `reference:` declara explícitamente que un campo es un puntero intencional a la raíz de otro agregado. El campo sigue siendo un tipo primitivo (`String`, `Long`, etc.) — **no se genera ningún `@ManyToOne`**.
689
+
690
+ #### Sintaxis
691
+
692
+ ```yaml
693
+ fields:
694
+ - name: customerId
695
+ type: String
696
+ reference:
697
+ aggregate: Customer # Nombre del agregado referenciado (PascalCase) — obligatorio
698
+ module: customers # Módulo donde vive el agregado — opcional
699
+ - name: productId
700
+ type: String
701
+ reference:
702
+ aggregate: Product
703
+ module: catalog
704
+ ```
705
+
706
+ #### Comportamiento
707
+
708
+ - El tipo Java **no cambia** — sigue siendo `String`, `Long`, UUID, etc.
709
+ - JPA genera `@Column` normal — **sin** `@ManyToOne` ni `@JoinColumn`.
710
+ - En la entidad de dominio y en la entidad JPA se genera un **comentario Javadoc** que documenta la referencia.
711
+ - `module:` es opcional: puede omitirse si el agregado referenciado está en el mismo módulo.
712
+ - Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
713
+
714
+ #### Código Generado
715
+
716
+ ```java
717
+ // domain/models/entities/Order.java
718
+ /** Cross-aggregate reference → Customer (module: customers) */
719
+ private String customerId;
720
+ ```
721
+
722
+ ```java
723
+ // infrastructure/database/entities/OrderJpa.java
724
+ @Column(name = "customer_id")
725
+ /** Cross-aggregate reference → Customer (module: customers) */
726
+ private String customerId;
727
+ ```
728
+
729
+ #### Por qué no usar `@ManyToOne` entre agregados
730
+
731
+ En DDD cada agregado es una unidad transaccional independiente. Un `@ManyToOne` cruzando límites crea un único grafo JPA que viola los límites transaccionales y crea dependencias de carga invisibles. La referencia por ID es el patrón correcto: el handler que necesite los datos del otro agregado los obtiene explícitamente via su propio repositorio.
732
+
733
+ - **Ejemplo completo:** [examples/domain-multi-aggregate.yaml](../examples/domain-multi-aggregate.yaml)
734
+
735
+ ---
736
+
686
737
  ### Validaciones JSR-303
687
738
 
688
739
  Eva4j soporta anotaciones Bean Validation (JSR-303/Jakarta Validation) en campos del `domain.yaml`. Las validaciones se generan **únicamente en la capa de aplicación**: en el `Create<Aggregate>Command` y en los `Create<Entity>Dto` de entidades secundarias. **No se aplican a entidades de dominio** ni a campos con `readOnly: true`.
@@ -12,7 +12,7 @@ Este documento describe las mejoras planificadas para futuras versiones de eva4j
12
12
  - [Soft Delete Completo](#3-soft-delete-completo)
13
13
 
14
14
  ### � Media Prioridad
15
- - [Paginación en Queries](#4-paginación-en-queries)
15
+ - [Paginación en Queries](#4-paginación-en-queries)
16
16
  - [Optimistic Locking](#5-optimistic-locking)
17
17
  - [Read Models Separados](#6-read-models-separados-proyecciones)
18
18
  - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones) ✅
@@ -27,6 +27,10 @@ Este documento describe las mejoras planificadas para futuras versiones de eva4j
27
27
  ### ✅ Implementado
28
28
  - [Auditoría de Tiempo y Usuario](#13-auditoría-implementada)
29
29
  - [Validaciones JSR-303](#14-validaciones-jsr-303-implementado)
30
+ - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones)
31
+ - [Generación Incremental / Diff](#10-generación-incremental--diff)
32
+ - [Paginación en Queries](#4-paginación-en-queries)
33
+ - [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id-implementado)
30
34
 
31
35
  ---
32
36
 
@@ -175,15 +179,15 @@ public class OrderEventListener {
175
179
 
176
180
  ---
177
181
 
178
- ## 2. Aggregate Boundaries por ID
182
+ ## 2. Aggregate Boundaries por ID
179
183
 
180
184
  ### Descripción
181
185
 
182
- En DDD, las referencias entre agregados distintos deben realizarse **por ID**, nunca con `@ManyToOne` cruzado. Hoy eva4j genera referencias JPA directas entre todos los agregados del mismo módulo, creando un único grafo de entidades JPA en vez de agregados independientes.
186
+ Eva4j ya genera correctamente el patrón DDD de referencia por ID: los campos que apuntan a otro agregado se generan como tipos primitivos (`String`, `Long`, etc.) sin ningún `@ManyToOne` cruzado. Esta feature añade **declaración semántica explícita** mediante la propiedad `reference:` en el campo, que permite documentar la intención en el YAML y generar un comentario Javadoc en el código.
183
187
 
184
- Esto impide escalar los agregados de forma independiente y crea dependencias de carga que violan los límites transaccionales.
188
+ Sin `reference:`, un campo `customerId: String` es indistinguible de cualquier otro `String`. Con `reference:`, el generador sabe que es un puntero intencional al agregado `Customer` del módulo `customers`.
185
189
 
186
- ### Sintaxis Propuesta
190
+ ### Sintaxis
187
191
 
188
192
  ```yaml
189
193
  aggregates:
@@ -197,8 +201,8 @@ aggregates:
197
201
  - name: customerId
198
202
  type: String
199
203
  reference:
200
- aggregate: Customer
201
- module: customers
204
+ aggregate: Customer # Nombre del agregado (PascalCase) — obligatorio
205
+ module: customers # Módulo donde vive el agregado — opcional
202
206
  - name: productId
203
207
  type: String
204
208
  reference:
@@ -206,36 +210,45 @@ aggregates:
206
210
  module: catalog
207
211
  ```
208
212
 
213
+ ### Comportamiento
214
+
215
+ - El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc.
216
+ - JPA genera `@Column` normal — **sin** `@ManyToOne` ni `@JoinColumn`.
217
+ - Se genera un **comentario Javadoc** en la entidad de dominio y en la entidad JPA.
218
+ - `module:` es opcional: se puede omitir si el agregado referenciado está en el mismo módulo.
219
+ - Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
220
+
209
221
  ### Código Generado
210
222
 
211
223
  ```java
212
224
  // domain/models/entities/Order.java
213
- public class Order {
214
- private String id;
215
- private String customerId; // Solo el ID, nunca Customer customer
216
- private String productId;
217
- }
225
+ /** Cross-aggregate reference → Customer (module: customers) */
226
+ private String customerId;
227
+
228
+ /** Cross-aggregate reference → Product (module: catalog) */
229
+ private String productId;
218
230
  ```
219
231
 
220
232
  ```java
221
- public class GetOrderWithCustomerQueryHandler {
222
- private final OrderRepository orderRepository;
223
- private final CustomerServiceClient customerClient;
224
-
225
- public OrderWithCustomerDto handle(GetOrderWithCustomerQuery query) {
226
- Order order = orderRepository.findById(query.orderId()).orElseThrow();
227
- CustomerSummary customer = customerClient.findById(order.getCustomerId());
228
- return new OrderWithCustomerDto(order, customer);
229
- }
230
- }
233
+ // infrastructure/database/entities/OrderJpa.java
234
+ @Column(name = "customer_id")
235
+ /** Cross-aggregate reference → Customer (module: customers) */
236
+ private String customerId;
237
+
238
+ @Column(name = "product_id")
239
+ /** Cross-aggregate reference → Product (module: catalog) */
240
+ private String productId;
231
241
  ```
232
242
 
233
- ### Advertencia generada cuando se detecta cross-aggregate
243
+ ### Archivos Modificados
234
244
 
235
- ```
236
- ⚠️ WARNING: Order.customer uses a direct JPA reference to Customer aggregate.
237
- Consider using customerId (reference by ID) instead.
238
- ```
245
+ | Archivo | Cambio |
246
+ |---|---|
247
+ | `src/utils/yaml-to-entity.js` | ✅ Destructura y valida `reference:` en `parseProperty()` |
248
+ | `templates/aggregate/AggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
249
+ | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
250
+ | `templates/aggregate/JpaEntity.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
251
+ | `examples/domain-multi-aggregate.yaml` | ✅ Actualizado con `reference:` en `productId` y `warehouseId` |
239
252
 
240
253
  ---
241
254
 
@@ -320,74 +333,86 @@ public ResponseEntity<Void> restore(@PathVariable String id) {
320
333
 
321
334
  ---
322
335
 
323
- ## 4. Paginación en Queries
336
+ ## 4. Paginación en Queries
324
337
 
325
338
  ### Descripción
326
339
 
327
- Actualmente `ListQueryHandler` devuelve `List<T>` completo sin límite. En producción, cualquier colección que pueda crecer sin límite debe estar paginada. Esto también implica soporte para filtros dinámicos y ordenamiento.
340
+ Implementado como **paginación siempre activa** en todos los módulos generados. `GET /` ya no devuelve `List<T>` sin límite devuelve un `PagedResponse<T>` propio con `content`, `page`, `size`, `totalElements` y `totalPages`. Sin flags ni configuración adicional en `domain.yaml`.
328
341
 
329
- ### Sintaxis Propuesta
342
+ ### Implementación Realizada
330
343
 
331
- ```yaml
332
- aggregates:
333
- - name: Order
334
- queries:
335
- - name: FindAllOrders
336
- paginated: true
337
- filters:
338
- - field: status
339
- type: OrderStatus
340
- - field: customerId
341
- type: String
342
- sort:
343
- defaultField: createdAt
344
- defaultDirection: DESC
344
+ #### PagedResponse — `shared/application/dtos/PagedResponse.java`
345
+
346
+ Record genérico generado una vez por proyecto en la capa shared. Desacoplado de Spring Data `Page<T>` para no exponer internals de Spring en la API:
347
+
348
+ ```java
349
+ public record PagedResponse<T>(
350
+ List<T> content,
351
+ int page,
352
+ int size,
353
+ long totalElements,
354
+ int totalPages
355
+ ) {
356
+ public static <T> PagedResponse<T> of(
357
+ List<T> content, int page, int size, long totalElements) {
358
+ int totalPages = size == 0 ? 1 : (int) Math.ceil((double) totalElements / size);
359
+ return new PagedResponse<>(content, page, size, totalElements, totalPages);
360
+ }
361
+ }
345
362
  ```
346
363
 
347
- ### Código Generado
364
+ #### Query con parámetros de paginación
348
365
 
349
366
  ```java
350
367
  public record FindAllOrdersQuery(
351
- OrderStatus status,
352
- String customerId,
353
368
  int page,
354
369
  int size,
355
370
  String sortBy,
356
371
  String sortDirection
357
- ) {}
372
+ ) implements Query<PagedResponse<OrderResponseDto>> {}
358
373
  ```
359
374
 
375
+ #### Handler paginado
376
+
360
377
  ```java
361
- public Page<OrderResponseDto> handle(FindAllOrdersQuery query) {
362
- Pageable pageable = PageRequest.of(
363
- query.page(), query.size(),
364
- Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy())
365
- );
366
- Page<Order> orders = orderRepository.findAll(
367
- OrderSpecifications.build(query.status(), query.customerId()), pageable
368
- );
369
- return orders.map(mapper::toDto);
378
+ public PagedResponse<OrderResponseDto> handle(FindAllOrdersQuery query) {
379
+ Sort sort = Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy());
380
+ Pageable pageable = PageRequest.of(query.page(), query.size(), sort);
381
+ Page<Order> page = repository.findAll(pageable);
382
+ List<OrderResponseDto> content = page.getContent().stream().map(mapper::toDto).toList();
383
+ return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
370
384
  }
371
385
  ```
372
386
 
373
- ```java
374
- public class OrderSpecifications {
375
- public static Specification<OrderJpa> build(OrderStatus status, String customerId) {
376
- return Specification
377
- .where(hasStatus(status))
378
- .and(hasCustomerId(customerId));
379
- }
380
- private static Specification<OrderJpa> hasStatus(OrderStatus status) {
381
- return (root, query, cb) ->
382
- status == null ? null : cb.equal(root.get("status"), status);
383
- }
384
- private static Specification<OrderJpa> hasCustomerId(String customerId) {
385
- return (root, query, cb) ->
386
- customerId == null ? null : cb.equal(root.get("customerId"), customerId);
387
- }
387
+ #### Endpoint REST
388
+
389
+ ```bash
390
+ # Defaults: page=0, size=20, sortBy=id, sortDirection=ASC
391
+ GET /api/v1/orders?page=0&size=10&sortBy=createdAt&sortDirection=DESC
392
+
393
+ # Respuesta
394
+ {
395
+ "content": [...],
396
+ "page": 0,
397
+ "size": 10,
398
+ "totalElements": 87,
399
+ "totalPages": 9
388
400
  }
389
401
  ```
390
402
 
403
+ #### Archivos modificados
404
+
405
+ | Archivo | Cambio |
406
+ |---|---|
407
+ | `templates/shared/application/dtos/PagedResponse.java.ejs` | ✅ Nuevo template shared |
408
+ | `src/generators/shared-generator.js` | ✅ Método `generatePagedResponse()` |
409
+ | `src/commands/generate-entities.js` | ✅ Llama `generatePagedResponse` en cada `g entities` |
410
+ | `templates/crud/ListQuery.java.ejs` | ✅ Parámetros de paginación |
411
+ | `templates/crud/ListQueryHandler.java.ejs` | ✅ `PageRequest` + `PagedResponse` |
412
+ | `templates/aggregate/AggregateRepository.java.ejs` | ✅ `Page<X> findAll(Pageable)` |
413
+ | `templates/aggregate/AggregateRepositoryImpl.java.ejs` | ✅ Implementación `jpaRepository.findAll(pageable).map(...)` |
414
+ | `templates/crud/Controller.java.ejs` | ✅ `@RequestParam` page/size/sortBy/sortDirection |
415
+
391
416
  ---
392
417
 
393
418
  ## 5. Optimistic Locking
@@ -709,50 +734,50 @@ domain.yaml:41:7 error Relationship type "OneToFew" is not valid.
709
734
 
710
735
  ---
711
736
 
712
- ## 10. Generación Incremental / Diff
737
+ ## 10. Generación Incremental / Diff
713
738
 
714
739
  ### Descripción
715
740
 
716
- Actualmente `eva4j g entities` regenera **todos** los archivos, sobreescribiendo cualquier modificación manual. Este es el mayor bloqueador para adopción en proyectos reales donde el desarrollador personaliza la lógica generada.
741
+ Implementado como **safe mode con checksums SHA-256**. `eva4j g entities` (y `g usecase`, `g resource`) detecta si un archivo generado fue modificado manualmente después de su generación y lo omite automáticamente en re-ejecuciones. El flag `--force` permite sobreescribir cuando se desea regenerar intencionalmente.
717
742
 
718
- ### Estrategias Propuestas
743
+ ### Implementación Realizada
719
744
 
720
- #### Opción A: Archivos base + archivos de extensión
745
+ #### ChecksumManager `src/utils/checksum-manager.js`
721
746
 
722
- ```
723
- order/application/mappers/
724
- ├── OrderApplicationMapperBase.java <- regenerado siempre
725
- └── OrderApplicationMapper.java <- creado una vez, el dev lo personaliza
726
- ```
747
+ Almacena hashes SHA-256 de cada archivo escrito en un archivo `.eva4j-checksums.json` por módulo (junto al `domain.yaml`). Métodos clave:
748
+ - `wasModified(destPath, generatedContent)` — compara hash en disco vs hash almacenado
749
+ - `recordWrite(destPath, content)` — registra hash del archivo recién escrito
750
+ - `save()` persiste la base de datos de checksums
727
751
 
728
- ```java
729
- // OrderApplicationMapperBase.java — regenerado en cada g entities
730
- public abstract class OrderApplicationMapperBase {
731
- public Order fromCommand(CreateOrderCommand command) { /* generado */ }
732
- public OrderResponseDto toDto(Order entity) { /* generado */ }
733
- }
734
-
735
- // OrderApplicationMapper.java — creado una sola vez
736
- @Component
737
- public class OrderApplicationMapper extends OrderApplicationMapperBase {
738
- // Override opcional de métodos base
739
- // Métodos adicionales del proyecto aquí
740
- }
741
- ```
742
-
743
- #### Opción B: Flag --safe en el CLI
752
+ #### Safe mode en `renderAndWrite()` — `src/utils/template-engine.js`
744
753
 
745
754
  ```bash
746
- # Regenera solo archivos que NO fueron modificados manualmente
747
- eva4j g entities order --safe
755
+ # Comportamiento por defecto (safe mode)
756
+ eva4j g entities orders
748
757
 
749
758
  # Output:
750
- # OK Order.java -- regenerado (sin cambios previos)
751
- # OK OrderJpa.java -- regenerado (sin cambios previos)
752
- # SKIP OrderApplicationMapper.java -- omitido (modificado manualmente)
753
- # SKIP CreateOrderCommandHandler.java -- omitido (modificado manualmente)
759
+ # Order.java -- regenerado (sin cambios previos)
760
+ # OrderJpa.java -- regenerado (sin cambios previos)
761
+ # ⚠️ SKIP OrderApplicationMapper.java -- omitido (modificado manualmente — use --force to overwrite)
762
+ # ⚠️ SKIP CreateOrderCommandHandler.java -- omitido (modificado manualmente)
763
+
764
+ # Con --force: sobreescribe todo
765
+ eva4j g entities orders --force
754
766
  ```
755
767
 
768
+ #### Comandos con safe mode integrado
769
+
770
+ | Comando | Estado |
771
+ |---|---|
772
+ | `eva4j g entities <module>` | ✅ Integrado |
773
+ | `eva4j g usecase <module> <name>` | ✅ Integrado |
774
+ | `eva4j g resource <module>` | ✅ Integrado |
775
+ | `eva4j create` / `eva4j add module` | ⚠️ Out of scope (archivos de scaffolding inicial, no se re-ejecutan) |
776
+
777
+ #### Nota sobre portabilidad
778
+
779
+ `.eva4j-checksums.json` está en `.gitignore` por diseño — es estado local de la máquina de desarrollo. En un `git clone` fresco, la primera re-ejecución regenerará todos los archivos (comportamiento correcto en ese contexto).
780
+
756
781
  ---
757
782
 
758
783
  ## 11. Comando `eva4j doctor`
@@ -942,27 +967,109 @@ private Integer age;
942
967
 
943
968
  ---
944
969
 
970
+ ## 15. Transactional Outbox Pattern
971
+
972
+ ### Descripción
973
+
974
+ El **Transactional Outbox Pattern** es la evolución natural de los Domain Events implementados (ítem 1). Resuelve el caso donde el proceso muere después del commit de BD pero antes de que `ApplicationEventPublisher` llegue a publicar al broker externo — en ese escenario, el evento se pierde silenciosamente.
975
+
976
+ El patrón garantiza **at-least-once delivery**: los eventos son almacenados en la misma transacción que el agregado y un proceso separado los publica de forma resiliente.
977
+
978
+ Los Domain Events ya implementados (`ApplicationEventPublisher` + `@TransactionalEventListener(AFTER_COMMIT)`) son suficientes para la mayoría de sistemas. Esta feature es necesaria para dominios críticos: pagos, auditoría regulatoria, inventario en tiempo real.
979
+
980
+ **Nota:** El puerto `MessageBroker` ya generado no requiere cambios — solo se añade la capa de persistencia intermedia.
981
+
982
+ ### Flujo del Patrón
983
+
984
+ ```
985
+ BD Transaction:
986
+ → INSERT INTO orders ...
987
+ → INSERT INTO outbox_events (type, payload, published=false) ← misma TX
988
+ → COMMIT
989
+
990
+ Proceso resiliente (polling o CDC con Debezium):
991
+ → SELECT * FROM outbox_events WHERE published = false
992
+ → Publica a Kafka / RabbitMQ / SNS
993
+ → UPDATE outbox_events SET published = true
994
+ ```
995
+
996
+ ### Sintaxis Propuesta en domain.yaml
997
+
998
+ ```yaml
999
+ aggregates:
1000
+ - name: Order
1001
+ events:
1002
+ - name: OrderPlaced
1003
+ kafka: true
1004
+ delivery: at-least-once # ← activa Outbox Pattern para este evento
1005
+ fields:
1006
+ - name: customerId
1007
+ type: String
1008
+ ```
1009
+
1010
+ ### Código Generado (Outbox Table + Publisher)
1011
+
1012
+ ```java
1013
+ @Entity
1014
+ @Table(name = "outbox_events")
1015
+ public class OutboxEvent {
1016
+ @Id
1017
+ private String id;
1018
+ private String aggregateType;
1019
+ private String aggregateId;
1020
+ private String eventType;
1021
+ @Column(columnDefinition = "TEXT")
1022
+ private String payload; // JSON serializado del evento
1023
+ private boolean published = false;
1024
+ private LocalDateTime createdAt;
1025
+ private LocalDateTime publishedAt;
1026
+ }
1027
+ ```
1028
+
1029
+ ```java
1030
+ // OutboxEventPublisher — proceso de polling (cada 5s via @Scheduled)
1031
+ @Component
1032
+ public class OutboxEventPublisher {
1033
+ @Scheduled(fixedDelay = 5000)
1034
+ @Transactional
1035
+ public void publishPendingEvents() {
1036
+ List<OutboxEvent> pending = outboxRepository.findByPublishedFalse();
1037
+ pending.forEach(event -> {
1038
+ messageBroker.publishRaw(event.getEventType(), event.getPayload());
1039
+ event.markPublished();
1040
+ });
1041
+ }
1042
+ }
1043
+ ```
1044
+
1045
+ ### Prerrequisito
1046
+
1047
+ Domain Events (ítem 1) implementados y funcionando — este ítem solo añade persistencia intermedia, no reemplaza la arquitectura existente.
1048
+
1049
+ ---
1050
+
945
1051
  ## Resumen de Prioridades
946
1052
 
947
1053
  | # | Característica | Prioridad | Complejidad | Estado |
948
1054
  |---|---|---|---|---|
949
- | 1 | Domain Events | Alta | Alta | Pendiente |
950
- | 2 | Aggregate Boundaries por ID | Alta | Media | Pendiente |
951
- | 3 | Soft Delete Completo | Alta | Baja | Parcial |
952
- | 4 | Paginación en Queries | Media | Media | Pendiente |
953
- | 5 | Optimistic Locking | Media | Baja | Pendiente |
954
- | 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
1055
+ | 1 | Domain Events | Alta | Alta | Implementado |
1056
+ | 2 | Aggregate Boundaries por ID | Alta | Media | Implementado |
1057
+ | 3 | Soft Delete Completo | Alta | Baja | Parcial |
1058
+ | 4 | Paginación en Queries | Impl. | -- | Implementado |
1059
+ | 5 | Optimistic Locking | Media | Baja | Pendiente |
1060
+ | 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
955
1061
  | 7 | Enums con Transiciones | Impl. | -- | ✅ Implementado |
956
- | 8 | Specifications Pattern | Media | Media | Pendiente |
957
- | 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
958
- | 10 | Generacion Incremental | Tooling | Alta | Pendiente |
959
- | 11 | eva4j doctor | Tooling | Media | Pendiente |
960
- | 12 | Tests Completos | Tooling | Media | Pendiente |
1062
+ | 8 | Specifications Pattern | Media | Media | Pendiente |
1063
+ | 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
1064
+ | 10 | Generacion Incremental | Tooling | -- | Implementado |
1065
+ | 11 | eva4j doctor | Tooling | Media | Pendiente |
1066
+ | 12 | Tests Completos | Tooling | Media | Pendiente |
961
1067
  | 13 | Auditoria completa | Impl. | -- | ✅ Implementado |
962
1068
  | 14 | Validaciones JSR-303 | Impl. | -- | ✅ Implementado |
1069
+ | 15 | Transactional Outbox Pattern | Alta | Alta | Pendiente |
963
1070
 
964
1071
  ---
965
1072
 
966
- **Ultima actualizacion:** 2026-02-22
1073
+ **Ultima actualizacion:** 2026-02-25
967
1074
  **Version de eva4j:** 1.x
968
1075
  **Estado:** Documento de planificacion y referencia