eva4j 1.0.14 → 1.0.16

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 (139) hide show
  1. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  2. package/.agents/skills/skill-creator/SKILL.md +485 -0
  3. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  4. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  5. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  6. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  7. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  8. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  9. package/.agents/skills/skill-creator/references/schemas.md +430 -0
  10. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  11. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  12. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  13. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  14. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  15. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  16. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  17. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  18. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  19. package/AGENTS.md +268 -6
  20. package/COMMAND_EVALUATION.md +15 -16
  21. package/DOMAIN_YAML_GUIDE.md +430 -14
  22. package/FUTURE_FEATURES.md +1627 -1168
  23. package/README.md +461 -13
  24. package/bin/eva4j.js +32 -14
  25. package/config/defaults.json +1 -0
  26. package/docs/commands/EVALUATE_SYSTEM.md +746 -261
  27. package/docs/commands/EXPORT_DIAGRAM.md +153 -0
  28. package/docs/commands/GENERATE_ENTITIES.md +599 -6
  29. package/docs/commands/INDEX.md +7 -0
  30. package/examples/domain-events.yaml +166 -20
  31. package/examples/domain-listeners.yaml +212 -0
  32. package/examples/domain-one-to-many.yaml +1 -0
  33. package/examples/domain-one-to-one.yaml +1 -0
  34. package/examples/domain-ports.yaml +414 -0
  35. package/examples/domain-soft-delete.yaml +47 -44
  36. package/examples/system/notification.yaml +147 -0
  37. package/examples/system/product.yaml +185 -0
  38. package/examples/system/system.yaml +112 -0
  39. package/examples/system-report.html +971 -0
  40. package/examples/system.yaml +46 -3
  41. package/package.json +2 -1
  42. package/src/agents/design-reviewer.agent.md +163 -0
  43. package/src/commands/build.js +714 -0
  44. package/src/commands/create.js +1 -0
  45. package/src/commands/detach.js +149 -108
  46. package/src/commands/evaluate-system.js +234 -8
  47. package/src/commands/export-diagram.js +522 -0
  48. package/src/commands/generate-entities.js +685 -66
  49. package/src/commands/generate-http-exchange.js +2 -0
  50. package/src/commands/generate-kafka-event.js +43 -10
  51. package/src/generators/base-generator.js +18 -6
  52. package/src/generators/postman-generator.js +188 -0
  53. package/src/generators/shared-generator.js +12 -2
  54. package/src/skills/build-system-yaml/SKILL.md +323 -0
  55. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
  56. package/src/skills/build-system-yaml/references/module-spec.md +367 -0
  57. package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -0
  58. package/src/utils/config-manager.js +54 -0
  59. package/src/utils/context-builder.js +1 -0
  60. package/src/utils/domain-diagram.js +192 -0
  61. package/src/utils/domain-validator.js +1020 -0
  62. package/src/utils/fake-data.js +376 -0
  63. package/src/utils/system-validator.js +319 -199
  64. package/src/utils/yaml-to-entity.js +272 -7
  65. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  66. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  67. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
  68. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  69. package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
  70. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  71. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  72. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  73. package/templates/base/docker/Dockerfile.ejs +21 -0
  74. package/templates/base/gradle/build.gradle.ejs +3 -2
  75. package/templates/base/root/AGENTS.md.ejs +306 -45
  76. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  77. package/templates/crud/Controller.java.ejs +4 -4
  78. package/templates/crud/CreateCommand.java.ejs +4 -0
  79. package/templates/crud/CreateCommandHandler.java.ejs +3 -6
  80. package/templates/crud/CreateItemDto.java.ejs +4 -0
  81. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  82. package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
  83. package/templates/crud/EndpointsController.java.ejs +6 -6
  84. package/templates/crud/FindByQuery.java.ejs +1 -1
  85. package/templates/crud/FindByQueryHandler.java.ejs +3 -9
  86. package/templates/crud/GetQueryHandler.java.ejs +2 -5
  87. package/templates/crud/ListQuery.java.ejs +1 -1
  88. package/templates/crud/ListQueryHandler.java.ejs +8 -14
  89. package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
  90. package/templates/crud/ScaffoldQuery.java.ejs +3 -2
  91. package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
  92. package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
  93. package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
  94. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
  95. package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
  96. package/templates/crud/UpdateCommand.java.ejs +4 -0
  97. package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
  98. package/templates/evaluate/report.html.ejs +394 -2
  99. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  100. package/templates/kafka-event/Event.java.ejs +9 -0
  101. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  102. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  103. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  104. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  105. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +25 -0
  106. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  107. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  108. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  109. package/templates/mock/MockEvent.java.ejs +10 -0
  110. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  111. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  112. package/templates/mock/SpringEventListener.java.ejs +61 -0
  113. package/templates/ports/PortDomainModel.java.ejs +35 -0
  114. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  115. package/templates/ports/PortFeignClient.java.ejs +45 -0
  116. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  117. package/templates/ports/PortInterface.java.ejs +45 -0
  118. package/templates/ports/PortNestedType.java.ejs +28 -0
  119. package/templates/ports/PortRequestDto.java.ejs +30 -0
  120. package/templates/ports/PortResponseDto.java.ejs +28 -0
  121. package/templates/postman/Collection.json.ejs +1 -1
  122. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  123. package/templates/shared/annotations/LogAfter.java.ejs +2 -0
  124. package/templates/shared/annotations/LogBefore.java.ejs +2 -0
  125. package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
  126. package/templates/shared/annotations/LogLevel.java.ejs +8 -0
  127. package/templates/shared/annotations/LogTimer.java.ejs +1 -0
  128. package/templates/shared/annotations/Loggable.java.ejs +17 -0
  129. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
  130. package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
  131. package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
  132. package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
  133. package/src/commands/generate-system.js +0 -243
  134. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
  135. package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
  136. package/templates/base/root/skill-build-system-yaml.ejs +0 -252
  137. package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
  138. package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
  139. package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +0 -6
@@ -20,6 +20,8 @@
20
20
  14. [Complete examples](#14-complete-examples)
21
21
  15. [Prerequisites and common errors](#15-prerequisites-and-common-errors)
22
22
  16. [Declarative endpoints — use case patterns](#16-declarative-endpoints-endpoints--use-case-patterns)
23
+ 17. [Consuming external events (listeners:)](#17-consuming-external-events-listeners)
24
+ 18. [Synchronous HTTP clients (ports:)](#18-synchronous-http-clients-ports)
23
25
 
24
26
  ---
25
27
 
@@ -376,6 +378,84 @@ entities:
376
378
 
377
379
  ---
378
380
 
381
+ ## 7b. Soft Delete
382
+
383
+ When `hasSoftDelete: true` is set on the aggregate root, eva4j generates logical deletion instead of physical: the record is not removed from the database but stamped with a `deletedAt` timestamp.
384
+
385
+ > **Scope rule:** `hasSoftDelete` is **only valid on the aggregate root** (`isRoot: true`). Setting it on a secondary entity emits a warning and is silently ignored — secondary entities are deleted physically via CASCADE from the root.
386
+
387
+ ### Syntax
388
+
389
+ ```yaml
390
+ entities:
391
+ - name: order
392
+ isRoot: true # ← mandatory: only root entities support this
393
+ tableName: orders
394
+ hasSoftDelete: true # ✅ enables soft delete
395
+ audit:
396
+ enabled: true
397
+ fields:
398
+ - name: id
399
+ type: String
400
+ - name: orderNumber
401
+ type: String
402
+
403
+ - name: orderItem # ← secondary entity
404
+ tableName: order_items
405
+ # hasSoftDelete: true ← ❌ ignored with warning
406
+ ```
407
+
408
+ ### What is generated
409
+
410
+ | Artefact | `deletedAt` included | Notes |
411
+ |---|---|---|
412
+ | Full constructor (reconstruction) | ✅ | Required to hydrate persisted state |
413
+ | Business constructor (new object) | ❌ | Starts as `null` |
414
+ | `CreateDto` / `CreateCommand` | ❌ | Cannot create an already-deleted object |
415
+ | `ResponseDto` | ❌ | Internal metadata, not exposed in API |
416
+ | `toJpa()` in mapper | ✅ | Persists the timestamp after `softDelete()` |
417
+
418
+ **Domain entity:**
419
+ ```java
420
+ public class Order {
421
+ private LocalDateTime deletedAt; // injected automatically
422
+
423
+ public void softDelete() {
424
+ if (this.deletedAt != null) {
425
+ throw new IllegalStateException("Order is already deleted");
426
+ }
427
+ this.deletedAt = java.time.LocalDateTime.now();
428
+ }
429
+
430
+ public boolean isDeleted() {
431
+ return this.deletedAt != null;
432
+ }
433
+ }
434
+ ```
435
+
436
+ **JPA entity:**
437
+ ```java
438
+ @SQLRestriction("deleted_at IS NULL") // filters ALL queries automatically
439
+ @Entity
440
+ @Table(name = "orders")
441
+ public class OrderJpa extends AuditableEntity {
442
+ private LocalDateTime deletedAt;
443
+ }
444
+ ```
445
+
446
+ **DeleteCommandHandler** (generated when `hasSoftDelete: true`):
447
+ ```java
448
+ // Finds → marks → saves — never deleteById
449
+ Order order = repository.findById(id)
450
+ .orElseThrow(() -> new OrderNotFoundException(id));
451
+ order.softDelete();
452
+ repository.save(order);
453
+ ```
454
+
455
+ > `deletedAt` **must not be defined manually** in `fields:` — the generator injects it automatically.
456
+
457
+ ---
458
+
379
459
  ## 8. Relationships
380
460
 
381
461
  ### Properties
@@ -625,27 +705,83 @@ Java condition evaluated in the transition method. If the expression is `true`,
625
705
 
626
706
  ## 11. Domain events
627
707
 
628
- Events are declared under the aggregate (at the same level as `entities:`, `enums:`, `valueObjects:`).
708
+ Events are declared under the aggregate (at the same level as `entities:`, `enums:`, `valueObjects:`). Use the optional `triggers` property to connect an event to one or more state transition methods — the generator then emits `raise()` automatically inside each method.
629
709
 
630
710
  ```yaml
631
711
  aggregates:
632
712
  - name: Order
713
+ enums:
714
+ - name: OrderStatus
715
+ initialValue: DRAFT
716
+ transitions:
717
+ - from: DRAFT
718
+ to: PLACED
719
+ method: place
720
+ - from: PLACED
721
+ to: CANCELLED
722
+ method: cancel
723
+ values: [DRAFT, PLACED, CANCELLED]
633
724
  events:
634
725
  - name: OrderPlaced
726
+ topic: ORDER_PLACED # preferred: explicit — must match listeners[].topic in consumers
727
+ # default derivation: strips 'Event' suffix → OrderPlacedEvent → ORDER_PLACED
728
+ triggers:
729
+ - place # transition method name that publishes this event
635
730
  fields:
731
+ # orderId declared for cross-module Kafka consumers — generator maps it to event.getAggregateId()
732
+ - name: orderId
733
+ type: String
636
734
  - name: customerId
637
735
  type: String
638
736
  - name: totalAmount
639
737
  type: BigDecimal
738
+ - name: placedAt
739
+ type: LocalDateTime
640
740
  - name: OrderCancelled
741
+ topic: ORDER_CANCELLED # preferred: explicit topic
742
+ triggers:
743
+ - cancel
641
744
  fields:
642
- - name: reason
745
+ - name: reason # unresolvable → null /* TODO: provide reason */
643
746
  type: String
644
747
  entities:
645
748
  - name: Order
749
+ isRoot: true
646
750
  # ...
647
751
  ```
648
752
 
753
+ ### `triggers` — argument resolution rules (in order)
754
+
755
+ | Field condition | Generated argument |
756
+ |---|---|
757
+ | Always (first arg — `aggregateId` from `DomainEvent` base) | `this.getId()` |
758
+ | Name = `{entityName}Id` (e.g. `orderId` in `Order`) | **Skipped in Domain Event class** — mapped to `event.getAggregateId()` in the Integration Event handler |
759
+ | Name matches a field of the entity | `this.get{Field}()` |
760
+ | Name ends in `At` + type `LocalDateTime` | `LocalDateTime.now()` |
761
+ | Not resolvable | `null /* TODO: provide {fieldName} */` |
762
+
763
+ > **Convention:** Declare `{entityName}Id` in `events[].fields` when the event **crosses module boundaries via Kafka** — it is required so the id travels in the Integration Event payload. The generator automatically maps it to `event.getAggregateId()` in the handler, preventing duplication in the internal Domain Event class. If the event is only consumed within the same bounded context (Spring event bus), `{entityName}Id` can be omitted since `getAggregateId()` is already available.
764
+
765
+ ### `topic` — Kafka topic name for the event
766
+
767
+ Optional but **recommended**. Declares the Kafka topic name for this event explicitly.
768
+
769
+ ```yaml
770
+ events:
771
+ - name: OrderPlacedEvent
772
+ topic: ORDER_PLACED # ✅ preferred: explicit, matches listeners[].topic in consumers
773
+ triggers: [place]
774
+ fields: [...]
775
+ ```
776
+
777
+ **Default derivation (when `topic:` is omitted):** the generator strips the `Event` suffix from the class name before converting to SCREAMING_SNAKE_CASE:
778
+ - `OrderPlacedEvent` → `ORDER_PLACED` ✓
779
+ - `OrderCancelled` *(no suffix)* → `ORDER_CANCELLED` ✓
780
+
781
+ **Why prefer explicit `topic:`:** when a consumer module declares `listeners[].topic: ORDER_PLACED`, the value must match the producer's topic exactly. Declaring it explicitly in both places eliminates any risk of mismatch and makes the contract visible without having to understand the derivation rule.
782
+
783
+ If an event has **no `triggers`**, the developer must call `raise()` manually inside the business method.
784
+
649
785
  ### Generated files
650
786
 
651
787
  | File | Description |
@@ -659,29 +795,52 @@ aggregates:
659
795
 
660
796
  ### Generated event
661
797
 
798
+ Fields declared as `{entityName}Id` are excluded from the **Domain Event class** (the aggregate id is already available via `getAggregateId()` inherited from `DomainEvent`), but they **are included** in the Integration Event record and the Kafka payload — the handler maps them to `event.getAggregateId()` automatically.
799
+
662
800
  ```java
663
801
  public final class OrderPlaced extends DomainEvent {
802
+ // aggregateId (= orderId) inherited from DomainEvent — not repeated here
664
803
  private final String customerId;
665
804
  private final BigDecimal totalAmount;
805
+ private final LocalDateTime placedAt;
666
806
 
667
- public OrderPlaced(String customerId, BigDecimal totalAmount) {
807
+ public OrderPlaced(String aggregateId, String customerId, BigDecimal totalAmount, LocalDateTime placedAt) {
808
+ super(aggregateId);
668
809
  this.customerId = customerId;
669
810
  this.totalAmount = totalAmount;
811
+ this.placedAt = placedAt;
670
812
  }
671
813
 
672
814
  // getters
673
815
  }
674
816
  ```
675
817
 
676
- ### How to raise an event in the entity
818
+ ### How to raise an event auto-generated via `triggers`
819
+
820
+ When `triggers` is declared, the generator emits the `raise()` call automatically inside each transition method:
821
+
822
+ ```java
823
+ public void place() {
824
+ this.status = this.status.transitionTo(OrderStatus.PLACED);
825
+ raise(new OrderPlaced(this.getId(), this.getCustomerId(), this.getTotalAmount(), LocalDateTime.now()));
826
+ // ^—aggregateId ^—customerId ^—totalAmount ^—placedAt
827
+ }
828
+
829
+ public void cancel() {
830
+ this.status = this.status.transitionTo(OrderStatus.CANCELLED);
831
+ raise(new OrderCancelled(this.getId(), null /* TODO: provide reason */));
832
+ }
833
+ ```
834
+
835
+ For events **without `triggers`**, call `raise()` manually:
677
836
 
678
837
  ```java
679
838
  public class Order {
680
839
  private final List<DomainEvent> domainEvents = new ArrayList<>();
681
840
 
682
- public void place(String customerId, BigDecimal totalAmount) {
841
+ public void someBusinessAction() {
683
842
  // business logic...
684
- raise(new OrderPlaced(customerId, totalAmount));
843
+ raise(new OrderPlaced(this.getId(), this.customerId, this.totalAmount, LocalDateTime.now()));
685
844
  }
686
845
 
687
846
  protected void raise(DomainEvent event) {
@@ -696,6 +855,14 @@ public class Order {
696
855
  }
697
856
  ```
698
857
 
858
+ ### Validator checks
859
+
860
+ | Code | Severity | Condition |
861
+ |------|----------|-----------|
862
+ | C2-001 | warning | Transition without a use-case — silenced when `triggers` is present |
863
+ | C2-004 | error | `triggers` references a method that does not exist in any transition |
864
+ | C2-005 | info | Transition method with no associated event — consider adding `triggers` |
865
+
699
866
  ---
700
867
 
701
868
  ## 12. Multiple aggregates
@@ -838,10 +1005,19 @@ aggregates:
838
1005
 
839
1006
  events:
840
1007
  - name: OrderPlaced
1008
+ triggers:
1009
+ - confirm
841
1010
  fields:
1011
+ # orderId declared for cross-module Kafka consumers — generator maps it to event.getAggregateId()
1012
+ - name: orderId
1013
+ type: String
842
1014
  - name: customerId
843
1015
  type: String
1016
+ - name: confirmedAt
1017
+ type: LocalDateTime
844
1018
  - name: OrderCancelled
1019
+ triggers:
1020
+ - cancel
845
1021
  fields:
846
1022
  - name: reason
847
1023
  type: String
@@ -1103,3 +1279,420 @@ This is intentional: the developer fills in the custom business logic while the
1103
1279
  | Pattern `{FieldPascal}` | `toPascalCase(fieldName)` | `customerId` → `CustomerId` |
1104
1280
  | Sub-entity target | PascalCase (must match entity `name:`) | `OrderItem` |
1105
1281
 
1282
+ ---
1283
+
1284
+ ## 17. Consuming external events (`listeners:`)
1285
+
1286
+ The `listeners:` section declares the integration events that this module **consumes** from external producers. It lives at the **root level** of `domain.yaml` as a sibling of `aggregates:`, because it is an infrastructure/integration concern rather than a domain concern.
1287
+
1288
+ > **Requires a broker.** Files are only generated when `eva add kafka-client` has been executed in the project. Without a broker, the section is silently ignored.
1289
+
1290
+ ### Syntax
1291
+
1292
+ ```yaml
1293
+ # Root level — sibling of aggregates:
1294
+ listeners:
1295
+ - event: PaymentApprovedEvent # PascalCase + Event suffix
1296
+ producer: payments # Module that produces the event (documentary only)
1297
+ topic: PAYMENT_APPROVED # Kafka topic — required in standalone modules
1298
+ useCase: ConfirmOrder # Use case invoked when the event is received
1299
+ fields: # Payload fields of the Integration Event
1300
+ - name: orderId
1301
+ type: String
1302
+ - name: approvedAt
1303
+ type: LocalDateTime
1304
+ - name: details
1305
+ type: PaymentDetails # Object field → declare in nestedTypes:
1306
+ nestedTypes: # Optional: auxiliary records for object-typed fields
1307
+ - name: paymentDetails # camelCase → PascalCase in the generated record
1308
+ fields:
1309
+ - name: paymentId
1310
+ type: String
1311
+ - name: amount
1312
+ type: BigDecimal
1313
+ ```
1314
+
1315
+ ### Properties
1316
+
1317
+ | Property | Required | Description |
1318
+ |----------|----------|-------------|
1319
+ | `event` | ✅ | Event name in PascalCase with `Event` suffix |
1320
+ | `producer` | ✅ | Module that produces the event (documentary reference, no code generated) |
1321
+ | `topic` | ✅ | Kafka topic. With `system.yaml` can be inferred; in **standalone** modules it is mandatory. |
1322
+ | `useCase` | ✅ | Use case name that handles the event (PascalCase) |
1323
+ | `fields` | ✅ | Payload fields; generates the `IntegrationEvent` record and types the dispatched Command |
1324
+ | `nestedTypes` | ❌ | Auxiliary record classes for object-typed fields in `fields:`. Each entry generates a separate `.java` record in `application/events/`. |
1325
+
1326
+ ### Generated files
1327
+
1328
+ For each `listeners:` entry, eva4j generates **5 files** (plus one record per `nestedTypes:` entry):
1329
+
1330
+ | # | File | Location | Description |
1331
+ |---|------|----------|-------------|
1332
+ | 0 | `{NestedName}.java` *(per nestedType)* | `application/events/` | Auxiliary record for object-typed fields |
1333
+ | 1 | `{Name}IntegrationEvent.java` | `application/events/` | Typed contract record (documentation + tests) |
1334
+ | 2 | `{Name}KafkaListener.java` | `infrastructure/kafkaListener/` | `@KafkaListener` — deserializes and dispatches |
1335
+ | 3 | `kafka.yaml` *(all envs)* | `resources/parameters/*/` | Topic registered under `topics:` |
1336
+ | 4 | `{UseCase}Command.java` | `application/commands/` | Typed command dispatched from the listener |
1337
+ | 5 | `{UseCase}CommandHandler.java` | `application/usecases/` | Handler stub (implement business logic here) |
1338
+
1339
+ ### Generated code example
1340
+
1341
+ **`PaymentDetails.java`** — `application/events/` (from `nestedTypes:`)
1342
+ ```java
1343
+ public record PaymentDetails(
1344
+ String paymentId,
1345
+ BigDecimal amount
1346
+ ) {}
1347
+ ```
1348
+
1349
+ **`PaymentApprovedIntegrationEvent.java`** — `application/events/`
1350
+ ```java
1351
+ public record PaymentApprovedIntegrationEvent(
1352
+ String orderId,
1353
+ LocalDateTime approvedAt,
1354
+ PaymentDetails details
1355
+ ) {}
1356
+ ```
1357
+
1358
+ **`ConfirmOrderCommand.java`** — `application/commands/`
1359
+ ```java
1360
+ import com.example.orders.application.events.PaymentDetails;
1361
+
1362
+ public record ConfirmOrderCommand(
1363
+ String orderId,
1364
+ LocalDateTime approvedAt,
1365
+ PaymentDetails details
1366
+ ) implements Command {}
1367
+ ```
1368
+
1369
+ **`PaymentApprovedKafkaListener.java`** — `infrastructure/kafkaListener/`
1370
+ ```java
1371
+ import com.example.orders.application.events.PaymentDetails;
1372
+
1373
+ @Component
1374
+ public class PaymentApprovedKafkaListener {
1375
+
1376
+ private final UseCaseMediator useCaseMediator;
1377
+ private final ObjectMapper objectMapper;
1378
+
1379
+ @Value("${topics.payment-approved}")
1380
+ private String paymentApprovedTopic;
1381
+
1382
+ public PaymentApprovedKafkaListener(UseCaseMediator useCaseMediator,
1383
+ ObjectMapper objectMapper) {
1384
+ this.useCaseMediator = useCaseMediator;
1385
+ this.objectMapper = objectMapper;
1386
+ }
1387
+
1388
+ @KafkaListener(topics = "${topics.payment-approved}")
1389
+ public void handle(EventEnvelope<Map<String, Object>> event, Acknowledgment ack) {
1390
+ String orderId = objectMapper.convertValue(event.data().get("orderId"), String.class);
1391
+ LocalDateTime approvedAt = objectMapper.convertValue(
1392
+ event.data().get("approvedAt"), LocalDateTime.class);
1393
+ PaymentDetails details = objectMapper.convertValue(
1394
+ event.data().get("details"), PaymentDetails.class);
1395
+ useCaseMediator.dispatch(new ConfirmOrderCommand(orderId, approvedAt, details));
1396
+ ack.acknowledge();
1397
+ }
1398
+ }
1399
+ ```
1400
+
1401
+ **`ConfirmOrderCommandHandler.java`** — `application/usecases/`
1402
+ ```java
1403
+ @ApplicationComponent
1404
+ public class ConfirmOrderCommandHandler implements CommandHandler<ConfirmOrderCommand> {
1405
+
1406
+ @Override
1407
+ public void handle(ConfirmOrderCommand command) {
1408
+ // TODO: implement ConfirmOrder business logic
1409
+ throw new UnsupportedOperationException("ConfirmOrderCommandHandler not yet implemented");
1410
+ }
1411
+ }
1412
+ ```
1413
+
1414
+ ### `topic:` resolution rule
1415
+
1416
+ | Scenario | Behaviour |
1417
+ |----------|-----------|
1418
+ | Standalone module (only `domain.yaml`) | `topic:` **required** — no other source of truth |
1419
+ | Project with `system.yaml` | `topic:` optional; inferred from `integrations.async[].topic` |
1420
+ | `topic:` declared explicitly with `system.yaml` | Declared value takes **precedence** over inference |
1421
+
1422
+ ### Deserialization — how it works
1423
+
1424
+ The Kafka consumer delivers an `EventEnvelope<Map<String, Object>>`. The generated listener extracts each field using `objectMapper.convertValue()`, which handles:
1425
+
1426
+ - Primitives and strings: `convertValue(map.get("field"), String.class)`
1427
+ - Dates: `convertValue(map.get("date"), LocalDateTime.class)` — requires Jackson JavaTime module
1428
+ - Objects / nested types: `convertValue(map.get("details"), PaymentDetails.class)` — works automatically for Java records
1429
+ - Lists: `convertValue(map.get("items"), typeFactory.constructCollectionType(List.class, ItemType.class))`
1430
+
1431
+ ### `nestedTypes:` — when to use it
1432
+
1433
+ Use `nestedTypes:` when one of the `fields:` entries is a structured object (not a primitive Java type). Each entry generates a Java record in `application/events/` that is automatically imported in both the `KafkaListener` and the `Command`.
1434
+
1435
+ The name is declared in `camelCase` and normalised to `PascalCase` by the generator:
1436
+ ```yaml
1437
+ nestedTypes:
1438
+ - name: paymentDetails # → PaymentDetails.java
1439
+ fields:
1440
+ - name: paymentId
1441
+ type: String
1442
+ - name: amount
1443
+ type: BigDecimal
1444
+ ```
1445
+
1446
+ `{Name}IntegrationEvent.java` does **not** need an import for nested types because it lives in the same `application/events/` package.
1447
+
1448
+ ### Contrast: producing vs. consuming
1449
+
1450
+ ```
1451
+ domain.yaml
1452
+ ├── aggregates:
1453
+ │ └── [Aggregate]
1454
+ │ └── events: → Domain Events that this module PRODUCES (domain/models/events/)
1455
+
1456
+ └── listeners: → Integration Events that this module CONSUMES (infrastructure/kafkaListener/)
1457
+ ```
1458
+
1459
+ ---
1460
+
1461
+ ## 18. Synchronous HTTP clients (`ports:`)
1462
+
1463
+ The `ports:` section declares synchronous HTTP dependencies — external services this module **calls** via Feign. It lives at the **root level** of `domain.yaml` as a sibling of `aggregates:` and `listeners:`, because it represents a secondary port of the hexagonal architecture (not domain logic).
1464
+
1465
+ > **Requires OpenFeign.** The generated `FeignClient` and `FeignAdapter` depend on `spring-cloud-starter-openfeign`. The project must include it as a dependency.
1466
+
1467
+ ### 18.1 Syntax
1468
+
1469
+ ```yaml
1470
+ # Root level — sibling of aggregates: and listeners:
1471
+ # One entry = one method. Entries with the same service: form a single FeignClient.
1472
+
1473
+ ports:
1474
+ - name: findScreeningById # method name (camelCase)
1475
+ service: ScreeningService # groups into one interface/FeignClient (PascalCase)
1476
+ target: screenings # destination module (documentary reference only)
1477
+ baseUrl: http://localhost:8081 # → parameters/*/urls.yaml (first entry per service only)
1478
+ http: GET /screenings/{id} # HTTP verb + path
1479
+ fields: # response fields → domain model + infra DTO
1480
+ - name: id
1481
+ type: String
1482
+ - name: startTime
1483
+ type: LocalDateTime
1484
+
1485
+ - name: findAvailableSeats
1486
+ service: ScreeningService # same service → same FeignClient
1487
+ target: screenings
1488
+ http: GET /screenings/{id}/seats
1489
+ returnList: true # → List<Seat> in port interface
1490
+ domainType: Seat # override auto-derived type (default: FindAvailableSeat)
1491
+ fields:
1492
+ - name: seatId
1493
+ type: String
1494
+ - name: seatType
1495
+ type: String
1496
+
1497
+ - name: processPayment
1498
+ service: PaymentGateway
1499
+ target: payment-gateway-external
1500
+ baseUrl: https://api.payments.example.com
1501
+ http: POST /payments
1502
+ body: # @RequestBody → ProcessPaymentRequestDto.java
1503
+ - name: amount
1504
+ type: BigDecimal
1505
+ - name: paymentMethod
1506
+ type: PaymentMethodInput # object field → declare in nestedTypes:
1507
+ nestedTypes:
1508
+ - name: paymentMethodInput # camelCase → PascalCase record
1509
+ fields:
1510
+ - name: type
1511
+ type: String
1512
+ - name: cardToken
1513
+ type: String
1514
+ fields: # response fields → Payment domain model + infra DTO
1515
+ - name: paymentId
1516
+ type: String
1517
+ - name: status
1518
+ type: String
1519
+
1520
+ - name: cancelPayment
1521
+ service: PaymentGateway
1522
+ target: payment-gateway-external
1523
+ http: DELETE /payments/{id}
1524
+ # fields: omitted → void return
1525
+ ```
1526
+
1527
+ ### 18.2 Properties
1528
+
1529
+ | Property | Required | Description |
1530
+ |----------|----------|-------------|
1531
+ | `name` | ✅ | Method name in camelCase. Auto-derives the domain type (e.g. `findScreeningById` → `Screening`). |
1532
+ | `service` | ✅ | PascalCase service name. All entries sharing the same `service:` are grouped into one FeignClient. |
1533
+ | `target` | ✅ | Destination module or service name (documentary reference — no code generated). |
1534
+ | `baseUrl` | ❌ | Base URL for the service. Declare only on the **first entry** of each `service:`. Becomes a `urls.yaml` property. Defaults to `http://localhost:8080` with a warning if omitted. |
1535
+ | `http` | ✅ | `VERB /path` (e.g. `GET /users/{id}`, `POST /payments`). Path variables are `@PathVariable`. |
1536
+ | `fields` | ❌ | Response payload fields. Generates a domain model record and an infra DTO. Omit for `void` return. |
1537
+ | `domainType` | ❌ | Overrides the domain type name auto-derived from `name`. Use when the default derivation is ambiguous or wrong. |
1538
+ | `returnList` | ❌ | `true` → return type is `List<{DomainType}>` in the interface and `List<{InfraDto}>` in the FeignClient. Default: `false`. |
1539
+ | `body` | ❌ | Request body fields (POST/PUT/PATCH only). Generates a `{MethodPascal}RequestDto.java`. Ignored with a warning on GET/DELETE. |
1540
+ | `nestedTypes` | ❌ | Auxiliary record classes for object-typed fields in `body:`. Each entry generates a Java record in `application/dtos/`. |
1541
+
1542
+ ### 18.3 Domain type derivation
1543
+
1544
+ The domain type is derived automatically from the method name: the leading verb is stripped and the aggregate/entity name is extracted.
1545
+
1546
+ | Method name | Auto-derived type | Override needed? |
1547
+ |-------------|------------------|-----------------|
1548
+ | `findScreeningById` | `Screening` | No |
1549
+ | `findAvailableSeats` | `AvailableSeat` | Yes — `domainType: Seat` |
1550
+ | `processPayment` | `Payment` | No |
1551
+ | `findPaymentStatus` | `PaymentStatus` | Yes — avoids collision with `Payment` if both exist |
1552
+
1553
+ > **Rule:** if two methods in the same `service:` derive the same domain type, `generate entities` will fail. Declare `domainType:` on one of them to resolve the collision.
1554
+
1555
+ ### 18.4 Generated files
1556
+
1557
+ **Per unique `service:` name:**
1558
+
1559
+ | File | Description |
1560
+ |------|-------------|
1561
+ | `domain/repositories/{ServiceName}.java` | Secondary port interface — returns domain models |
1562
+ | `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | Typed Feign interface — returns infra DTOs |
1563
+ | `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` — ACL mapper |
1564
+ | `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Feign timeouts configuration |
1565
+ | `parameters/*/urls.yaml` | Base URL registered under a property key |
1566
+
1567
+ **Per unique domain model derived from methods with `fields:`:**
1568
+
1569
+ | File | Description |
1570
+ |------|-------------|
1571
+ | `domain/models/{service}/{DomainType}.java` | Domain model record (internal abstraction — ACL) |
1572
+
1573
+ **Per method:**
1574
+
1575
+ | File | Condition |
1576
+ |------|-----------|
1577
+ | `infrastructure/adapters/{service}/{MethodPascal}Dto.java` | When `fields:` present — infra DTO (external shape) |
1578
+ | `application/dtos/{MethodPascal}RequestDto.java` | When `body:` present (POST/PUT/PATCH) |
1579
+ | `application/dtos/{NestedTypePascal}.java` | When `nestedTypes:` declared |
1580
+
1581
+ ### 18.5 ACL pattern
1582
+
1583
+ The generated code follows the Anti-Corruption Layer (ACL) pattern to isolate the domain from external API shapes.
1584
+
1585
+ ```
1586
+ External API shape (infra DTO) Domain abstraction
1587
+ ──────────────────────────── ──────────────────
1588
+ infrastructure/adapters/{service}/ domain/models/{service}/
1589
+ {MethodPascal}Dto.java → {DomainType}.java
1590
+ {ServiceName}FeignClient.java → domain/repositories/{ServiceName}.java
1591
+ {ServiceName}FeignAdapter.java (maps DTO → domain model inline)
1592
+ ```
1593
+
1594
+ When the external API changes its field names or structure, **only the adapter is modified** — the domain model and the port interface stay stable.
1595
+
1596
+ ### 18.6 Generated code example
1597
+
1598
+ **`ScreeningService.java`** — `domain/repositories/` (port interface returning domain models)
1599
+ ```java
1600
+ public interface ScreeningService {
1601
+ Screening findScreeningById(String id);
1602
+ List<Seat> findAvailableSeats(String id);
1603
+ }
1604
+ ```
1605
+
1606
+ **`ScreeningServiceFeignClient.java`** — `infrastructure/adapters/screeningService/`
1607
+ ```java
1608
+ @FeignClient(name = "screeningService", url = "${urls.screening-service}")
1609
+ public interface ScreeningServiceFeignClient {
1610
+
1611
+ @GetMapping("/screenings/{id}")
1612
+ FindScreeningByIdDto findScreeningById(@PathVariable("id") String id);
1613
+
1614
+ @GetMapping("/screenings/{id}/seats")
1615
+ List<FindAvailableSeatsDto> findAvailableSeats(@PathVariable("id") String id);
1616
+ }
1617
+ ```
1618
+
1619
+ **`ScreeningServiceFeignAdapter.java`** — `infrastructure/adapters/screeningService/`
1620
+ ```java
1621
+ @Component
1622
+ public class ScreeningServiceFeignAdapter implements ScreeningService {
1623
+
1624
+ private final ScreeningServiceFeignClient feignClient;
1625
+
1626
+ public ScreeningServiceFeignAdapter(ScreeningServiceFeignClient feignClient) {
1627
+ this.feignClient = feignClient;
1628
+ }
1629
+
1630
+ @Override
1631
+ public Screening findScreeningById(String id) {
1632
+ return toScreening(feignClient.findScreeningById(id));
1633
+ }
1634
+
1635
+ @Override
1636
+ public List<Seat> findAvailableSeats(String id) {
1637
+ return feignClient.findAvailableSeats(id)
1638
+ .stream().map(this::toSeat).collect(Collectors.toList());
1639
+ }
1640
+
1641
+ private Screening toScreening(FindScreeningByIdDto dto) {
1642
+ return new Screening(dto.id(), dto.startTime());
1643
+ }
1644
+
1645
+ private Seat toSeat(FindAvailableSeatsDto dto) {
1646
+ return new Seat(dto.seatId(), dto.seatType());
1647
+ }
1648
+ }
1649
+ ```
1650
+
1651
+ **`Screening.java`** — `domain/models/screeningService/` (domain-side record)
1652
+ ```java
1653
+ public record Screening(String id, LocalDateTime startTime) {}
1654
+ ```
1655
+
1656
+ ### 18.7 `nestedTypes:` — when to use it
1657
+
1658
+ Use `nestedTypes:` under a port entry when one of the `body:` fields is a structured object (not a primitive). Each `nestedTypes:` entry generates a Java record in `application/dtos/` that is used by the request DTO.
1659
+
1660
+ ```yaml
1661
+ - name: processPayment
1662
+ service: PaymentGateway
1663
+ http: POST /payments
1664
+ body:
1665
+ - name: amount
1666
+ type: BigDecimal
1667
+ - name: paymentMethod
1668
+ type: PaymentMethodInput # object → declare in nestedTypes:
1669
+ nestedTypes:
1670
+ - name: paymentMethodInput # → PaymentMethodInput.java in application/dtos/
1671
+ fields:
1672
+ - name: type
1673
+ type: String
1674
+ - name: cardToken
1675
+ type: String
1676
+ ```
1677
+
1678
+ ### 18.8 `baseUrl:` resolution rule
1679
+
1680
+ | Scenario | Behaviour |
1681
+ |----------|-----------|
1682
+ | `baseUrl:` on the first entry of a `service:` | Registered in `parameters/*/urls.yaml` as `urls.{service-kebab}` |
1683
+ | `baseUrl:` omitted for all entries of a `service:` | Warning printed; defaults to `http://localhost:8080` |
1684
+ | `baseUrl:` on non-first entry of a `service:` | Ignored — only the first entry's value is used |
1685
+
1686
+ ### 18.9 Contrast: async vs sync
1687
+
1688
+ ```
1689
+ domain.yaml
1690
+ ├── aggregates:
1691
+ │ └── [Aggregate]
1692
+ │ └── events: → Domain Events that this module PRODUCES (async / broker)
1693
+
1694
+ ├── listeners: → Integration Events that this module CONSUMES (async / broker)
1695
+
1696
+ └── ports: → External HTTP services that this module CALLS (sync / Feign)
1697
+ ```
1698
+