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.
- package/.agents/skills/skill-creator/LICENSE.txt +202 -0
- package/.agents/skills/skill-creator/SKILL.md +485 -0
- package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
- package/.agents/skills/skill-creator/agents/comparator.md +202 -0
- package/.agents/skills/skill-creator/agents/grader.md +223 -0
- package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.agents/skills/skill-creator/references/schemas.md +430 -0
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
- package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
- package/.agents/skills/skill-creator/scripts/utils.py +47 -0
- package/AGENTS.md +268 -6
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +430 -14
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +461 -13
- package/bin/eva4j.js +32 -14
- package/config/defaults.json +1 -0
- package/docs/commands/EVALUATE_SYSTEM.md +746 -261
- package/docs/commands/EXPORT_DIAGRAM.md +153 -0
- package/docs/commands/GENERATE_ENTITIES.md +599 -6
- package/docs/commands/INDEX.md +7 -0
- package/examples/domain-events.yaml +166 -20
- package/examples/domain-listeners.yaml +212 -0
- package/examples/domain-one-to-many.yaml +1 -0
- package/examples/domain-one-to-one.yaml +1 -0
- package/examples/domain-ports.yaml +414 -0
- package/examples/domain-soft-delete.yaml +47 -44
- package/examples/system/notification.yaml +147 -0
- package/examples/system/product.yaml +185 -0
- package/examples/system/system.yaml +112 -0
- package/examples/system-report.html +971 -0
- package/examples/system.yaml +46 -3
- package/package.json +2 -1
- package/src/agents/design-reviewer.agent.md +163 -0
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +1 -0
- package/src/commands/detach.js +149 -108
- package/src/commands/evaluate-system.js +234 -8
- package/src/commands/export-diagram.js +522 -0
- package/src/commands/generate-entities.js +685 -66
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +43 -10
- package/src/generators/base-generator.js +18 -6
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +12 -2
- package/src/skills/build-system-yaml/SKILL.md +323 -0
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
- package/src/skills/build-system-yaml/references/module-spec.md +367 -0
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -0
- package/src/utils/config-manager.js +54 -0
- package/src/utils/context-builder.js +1 -0
- package/src/utils/domain-diagram.js +192 -0
- package/src/utils/domain-validator.js +1020 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/system-validator.js +319 -199
- package/src/utils/yaml-to-entity.js +272 -7
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
- package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
- package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
- package/templates/base/docker/Dockerfile.ejs +21 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- package/templates/crud/ApplicationMapper.java.ejs +4 -0
- package/templates/crud/Controller.java.ejs +4 -4
- package/templates/crud/CreateCommand.java.ejs +4 -0
- package/templates/crud/CreateCommandHandler.java.ejs +3 -6
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
- package/templates/crud/EndpointsController.java.ejs +6 -6
- package/templates/crud/FindByQuery.java.ejs +1 -1
- package/templates/crud/FindByQueryHandler.java.ejs +3 -9
- package/templates/crud/GetQueryHandler.java.ejs +2 -5
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -14
- package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
- package/templates/crud/ScaffoldQuery.java.ejs +3 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
- package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
- package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
- package/templates/evaluate/report.html.ejs +394 -2
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +9 -0
- package/templates/kafka-listener/KafkaController.java.ejs +1 -1
- package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
- package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
- package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
- package/templates/kafka-listener/ListenerCommandHandler.java.ejs +25 -0
- package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
- package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
- package/templates/mock/MockEvent.java.ejs +10 -0
- package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
- package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
- package/templates/mock/SpringEventListener.java.ejs +61 -0
- package/templates/ports/PortDomainModel.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +67 -0
- package/templates/ports/PortFeignClient.java.ejs +45 -0
- package/templates/ports/PortFeignConfig.java.ejs +24 -0
- package/templates/ports/PortInterface.java.ejs +45 -0
- package/templates/ports/PortNestedType.java.ejs +28 -0
- package/templates/ports/PortRequestDto.java.ejs +30 -0
- package/templates/ports/PortResponseDto.java.ejs +28 -0
- package/templates/postman/Collection.json.ejs +1 -1
- package/templates/postman/UnifiedCollection.json.ejs +185 -0
- package/templates/shared/annotations/LogAfter.java.ejs +2 -0
- package/templates/shared/annotations/LogBefore.java.ejs +2 -0
- package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
- package/templates/shared/annotations/LogLevel.java.ejs +8 -0
- package/templates/shared/annotations/LogTimer.java.ejs +1 -0
- package/templates/shared/annotations/Loggable.java.ejs +17 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
- package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
- package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
- package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
- package/src/commands/generate-system.js +0 -243
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
- package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
- package/templates/base/root/skill-build-system-yaml.ejs +0 -252
- package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
- package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
- 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
|
|
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
|
|
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
|
+
|